Compare commits
14
Commits
v0.9.0
...
67dbf25044
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67dbf25044 | ||
|
|
591c047229 | ||
|
|
7fc72c1cdb | ||
|
|
c2667b3bfe | ||
|
|
142a7fbaa9 | ||
|
|
84313b1955 | ||
|
|
839db3d4ca | ||
|
|
953908388d | ||
|
|
3f61b32219 | ||
|
|
3fdbfefa62 | ||
|
|
f496eb2f88 | ||
|
|
eddee2ec44 | ||
|
|
49b6c8a7e9 | ||
|
|
ebd3213a46 |
@@ -0,0 +1,22 @@
|
||||
# TEMPLATE DI PRODUZIONE per l'app CrAPP — copia in .env sul server e compila.
|
||||
# I valori Supabase vengono dal .env dello stack self-hosted in infra/supabase/docker/
|
||||
# (ANON_KEY -> VITE_SUPABASE_PUBLISHABLE_KEY, SERVICE_ROLE_KEY -> SUPABASE_SERVICE_ROLE_KEY).
|
||||
|
||||
VITE_SUPABASE_URL=https://tuodominio.it/api
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=
|
||||
SUPABASE_SERVICE_ROLE_KEY=
|
||||
|
||||
# CMS Strapi (rosa, classifica, storico partite, scout finalizzato) — vedi infra/strapi/.
|
||||
# VITE_STRAPI_URL è pubblico (letture aperte, come le tabelle Supabase "open RLS"), mentre
|
||||
# STRAPI_WRITE_TOKEN è un token con permessi di sola creazione su scout-match-finale: NON deve
|
||||
# mai avere il prefisso VITE_ (finirebbe nel bundle client) e va letto solo lato server.
|
||||
VITE_STRAPI_URL=https://tuodominio.it/cms
|
||||
STRAPI_WRITE_TOKEN=
|
||||
|
||||
# Notifiche push (genera con: npx web-push generate-vapid-keys)
|
||||
VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
VAPID_SUBJECT=mailto:tuamail@esempio.it
|
||||
|
||||
# Porta su cui ascolta il server Node generato da Nitro (preset node-server)
|
||||
PORT=3000
|
||||
@@ -39,3 +39,7 @@ dist-ssr
|
||||
|
||||
# Optional
|
||||
.vercel
|
||||
|
||||
# Supabase self-host: dati runtime, mai versionati
|
||||
infra/supabase/docker/volumes/db/data/
|
||||
infra/supabase/docker/volumes/storage/
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
CrAPP — a mobile-first web app for managing an amateur volleyball team (CRAP Volley): attendance,
|
||||
matches/trainings/events, live match scouting, player stats/badges, CSI league standings, push
|
||||
notifications. Italian-language codebase (routes, variables, comments). Built with Lovable
|
||||
(lovable.dev); pushes to `main` sync back into the Lovable editor — avoid rewriting published git
|
||||
history (force-push, rebase/amend/squash of pushed commits).
|
||||
|
||||
## Commands
|
||||
|
||||
Package manager is **bun** (`bun.lock` is the lockfile; `package-lock.json` also exists but bun is
|
||||
primary — see `bunfig.toml`).
|
||||
|
||||
```sh
|
||||
bun run dev # vite dev server
|
||||
bun run build # production build (nitro/vite)
|
||||
bun run build:dev # development-mode build
|
||||
bun run preview # preview a build
|
||||
bun run lint # eslint .
|
||||
bun run format # prettier --write .
|
||||
```
|
||||
|
||||
If `bun` isn't on PATH (sandboxed/CI environments), use the `npx`/`bunx` equivalents:
|
||||
`npx tsc --noEmit -p tsconfig.json` (typecheck, no npm script exists for this),
|
||||
`npx eslint src` (not `npx eslint .` — a full-repo scan chokes on the root-owned
|
||||
`infra/supabase/docker/volumes/db/data` directory), `npx prettier --write <files>`.
|
||||
|
||||
This repo is not fully `prettier`-clean — a fresh `eslint`/`prettier --check` run surfaces
|
||||
hundreds of pre-existing formatting errors unrelated to any given change. Filter with
|
||||
`grep -v "prettier/prettier"` to see real lint issues, and run `prettier --write` only on the
|
||||
files you touched, not the whole repo.
|
||||
|
||||
There is no test runner configured in this repo.
|
||||
|
||||
Adding a dependency: `bunfig.toml` enforces a 24h supply-chain guard (`minimumReleaseAge`) on new
|
||||
packages; only pre-listed `@lovable.dev/*` packages bypass it. Confirm with the user before adding
|
||||
new bypass entries.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Stack**: TanStack Start (React 19, file-based router) + Vite, styled with Tailwind v4 +
|
||||
shadcn/radix components, data via `@supabase/supabase-js`, TanStack Query for client cache.
|
||||
|
||||
### Routing
|
||||
|
||||
File-based routing under `src/routes/` (see `src/routes/README.md`). One root layout,
|
||||
`src/routes/__root.tsx`, wraps every page — preserve its `<Outlet />`. `$id.tsx` = dynamic segment,
|
||||
`{-$category}.tsx` = optional segment, `$.tsx` = splat (`_splat` param). Do not hand-create
|
||||
`src/pages/` or Next/Remix-style layout files. `src/routeTree.gen.ts` is auto-generated — never
|
||||
edit it directly.
|
||||
|
||||
Server-only HTTP endpoints live under `src/routes/api/public/*.ts` (e.g. push subscription,
|
||||
palloni/presenze reminders) — plain HTTP handlers, not proprietary edge functions, so they can run
|
||||
under any Node host or scheduler (system cron, pg_cron, etc).
|
||||
|
||||
### Server entry / SSR error handling
|
||||
|
||||
`src/start.ts` registers global middleware: `attachSupabaseAuth` (client-side function middleware
|
||||
that attaches the Supabase bearer token to every server-fn RPC — see
|
||||
`src/integrations/supabase/auth-attacher.ts`, which is **auto-generated**, do not hand-edit) and an
|
||||
error middleware + explicit CSRF middleware (`createCsrfMiddleware`) for server functions. Defining
|
||||
`src/start.ts` opts out of Start's automatic CSRF middleware, so this file must keep re-adding it.
|
||||
|
||||
`src/server.ts` wraps the generated TanStack Start server entry and normalizes a specific h3
|
||||
failure mode: h3 swallows in-handler throws into a 500 JSON body
|
||||
(`{"unhandled":true,"message":"HTTPError"}`) that a plain try/catch never sees, so it's detected by
|
||||
inspecting the response body and converted into `renderErrorPage()`'s HTML error page.
|
||||
|
||||
### Data layer — portability rule
|
||||
|
||||
The project must remain deployable on plain Node.js + PostgreSQL, not locked into Lovable Cloud
|
||||
(see `docs/PORTABILITA.md`). Concretely:
|
||||
|
||||
- **Never query the database (or the Strapi CMS) from components.** All data access goes through
|
||||
modules in `src/lib/*.ts` (e.g. `palloni.ts`, `mvp-voti.ts`, `eventi.ts`, `presenze.ts`, `rosa.ts`,
|
||||
`scout-*.ts`) — each exports TanStack Query hooks (`useX`) wrapping `supabase.from(...)` or, for
|
||||
Strapi-backed data, `strapiFetch(...)` from `src/lib/strapi-client.ts` (see `rosa-base.ts`,
|
||||
`classifica-csi.ts`, `storico-match.ts`). This keeps the backend swappable in one place.
|
||||
- `src/integrations/lovable/*` (social login) is optional and unused by any screen — safe to
|
||||
remove without impact.
|
||||
- No provider-exclusive features: no edge functions, no Lovable-only auth as the sole login method,
|
||||
no proprietary storage. Config only via standard env vars (`DATABASE_URL` / `SUPABASE_URL` +
|
||||
keys, `VAPID_*`), never hardcoded.
|
||||
- SQL migrations in `supabase/migrations/` must use standard PostgreSQL, no provider-exclusive
|
||||
extensions.
|
||||
|
||||
### Cloud-efficiency rules (small paid-tier budget)
|
||||
|
||||
The Supabase/Lovable Cloud plan is metered (~20 credits/month, ~17 users), so these constraints are
|
||||
load-bearing, not style preferences:
|
||||
|
||||
- No polling (`refetchInterval`) against the database; prefer local sync via
|
||||
BroadcastChannel/storage.
|
||||
- Global QueryClient (`src/router.tsx`) is deliberately configured for infrequent refetching:
|
||||
`staleTime` 5 min, `gcTime` 30 min, `refetchOnWindowFocus/Mount/Reconnect` all off, `retry: 1`.
|
||||
Don't override this per-query without a reason.
|
||||
- After a mutation, update the query cache with `setQueryData` (see `useAssegnaTurno` in
|
||||
`src/lib/palloni.ts` for the pattern) rather than `invalidateQueries`, to avoid an extra read.
|
||||
- Stats/badges/standings are "write once, read many": computed and persisted once (e.g. at match
|
||||
end), never recomputed on every page open.
|
||||
- Live match scouting: only the scoring user writes; everyone else reads already-saved data.
|
||||
- Player roster, CSI league standings and match history come from a self-hosted **Strapi CMS**
|
||||
(`infra/strapi/`, its own Postgres database on the same cluster as Supabase) — editing happens
|
||||
only in Strapi's own admin panel (`/cms/admin`), never in app UI. The app reads them read-only via
|
||||
`src/lib/rosa-base.ts` / `classifica-csi.ts` / `storico-match.ts` (long `staleTime`, no polling —
|
||||
same cloud-efficiency rules apply). Player identity is the stable `codice` field (`g1`, `g2`, …)
|
||||
on the Strapi `giocatore` content type, **not** Strapi's own numeric id — every Supabase table
|
||||
that references a player by id (`pagelle_voti`, `cacche_partita`, MVP votes, ball-duty turns,
|
||||
presenze, scout actions) keys on that string, so it must never change for an existing player.
|
||||
- A scouted match result (`ScoutMatch`, from `scout.tsx` / `scout-store.ts`) is saved to the
|
||||
scoring device's `localStorage` (key `crapp-scout-v1`, still the source `useRosa`/
|
||||
`giocatoriConScout` read for stats) **and** pushed best-effort to Strapi's `scout-match-finale`
|
||||
content type via `/api/public/scout-finale` at match end, so it becomes visible cross-device in
|
||||
the admin panel — no retry if that POST fails, the local save already succeeded either way. The
|
||||
in-progress resume state still goes only to the `scout_live` Supabase table, unrelated to Strapi.
|
||||
- Local dev quirk: this machine's `infra/supabase/docker/.env` may customize `POSTGRES_PORT`
|
||||
away from the documented default `5432` (e.g. to avoid colliding with another project's
|
||||
Postgres) — check that file before assuming the template's port when wiring up a new service
|
||||
to the same database.
|
||||
- Push notifications only for high-value events (convocations, training/match reminders, ball-duty
|
||||
turn, final result) — no chat/photo/video features.
|
||||
|
||||
### Auth
|
||||
|
||||
Supabase Auth (GoTrue), attached via middleware rather than per-request boilerplate — see
|
||||
`auth-attacher.ts` / `auth-middleware.ts` above. Self-hostable; not tied to Lovable-proprietary
|
||||
auth.
|
||||
|
||||
**Admin gating in-app** (`/scout`, `/eventi`, sollecito presenze, export CSV scout) no longer
|
||||
comes from picking a hardcoded player name — it's a client-side "admin mode" unlocked by logging
|
||||
into Strapi's real admin account (`src/lib/admin-auth.ts`'s `loginAdmin()`, calling Strapi's
|
||||
`/admin/login`; `useAdminSession()` reads a locally-stored flag with an 8h expiry, UI via
|
||||
`<AdminLogin />` in `/profilo` and inside the reserved screens). This still only gates the app's
|
||||
UI — Supabase tables involved (`eventi_app`, `scout_live`, etc.) keep their existing open RLS
|
||||
(anon can read/write directly via the public `anon` key), so it does not stop someone hitting the
|
||||
Supabase REST API directly. Closing that fully would mean real per-user Supabase Auth + RLS tied
|
||||
to it — there's a dormant, unused schema for this already in an early `supabase/migrations/*.sql`
|
||||
(`app_role` enum, `user_roles`, `has_role()`), superseded by the simpler open tables in use today.
|
||||
|
||||
## Project memory (`mem/`)
|
||||
|
||||
`mem/index.md` indexes feature-level design notes (`mem/features/*.md`) — currently the portability
|
||||
rule and the cloud-efficiency rules summarized above. Check there before adding a feature that
|
||||
might conflict with either constraint.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Language: Italian.** Code comments and git commit messages must be written in Italian, matching
|
||||
the rest of the codebase (routes, identifiers, existing comments/commits are already Italian).
|
||||
- Path alias `@/*` → `src/*` (see `tsconfig.json`).
|
||||
- Prettier: 100-char width, double quotes (`singleQuote: false`), trailing commas everywhere.
|
||||
- TypeScript strict mode plus extra strictness: `noUncheckedIndexedAccess`,
|
||||
`exactOptionalPropertyTypes`, `noImplicitReturns`, `noImplicitOverride`,
|
||||
`noPropertyAccessFromIndexSignature`.
|
||||
- `vite.config.ts` is intentionally minimal: `@lovable.dev/vite-tanstack-config` already bundles
|
||||
TanStack devtools, `tanstackStart`, `viteReact`, `tailwindcss`, `tsConfigPaths`, nitro, env
|
||||
injection, and the `@` alias — do not re-add any of those plugins manually or the app breaks with
|
||||
duplicate plugins.
|
||||
@@ -1,118 +1,215 @@
|
||||
# CRAP Volley Hub
|
||||
# CrAPP — CRAP Volley Hub
|
||||
|
||||
CrAPP – App per CRAP Volley
|
||||
App mobile-first per la gestione della squadra amatoriale di pallavolo **CRAP Volley**:
|
||||
presenze, partite/allenamenti/eventi, scouting live, statistiche giocatori, badge,
|
||||
classifica CSI, notifiche push.
|
||||
|
||||
Vorrei sviluppare un’app mobile per la squadra di pallavolo CRAP Volley, con nome CrAPP, disponibile per Android e iOS. L’obiettivo è creare un’app semplice da usare, moderna, bella da vedere e più coinvolgente rispetto a SportEasy, includendo anche funzionalità normalmente a pagamento in altre app.
|
||||
|
||||
Funzionalità principali
|
||||
|
||||
Gestione presenze/assenze
|
||||
|
||||
Partite
|
||||
|
||||
Allenamenti
|
||||
|
||||
Eventi extra
|
||||
|
||||
Stati rapidi: presente, assente, forse, in ritardo, indisponibile, infortunato
|
||||
|
||||
Statistiche giocatori
|
||||
|
||||
Presenze totali
|
||||
|
||||
Presenze consecutive
|
||||
|
||||
Gol/punti o altre statistiche specifiche della pallavolo
|
||||
|
||||
MVP, migliori performance, medie stagione
|
||||
|
||||
Statistiche partite
|
||||
|
||||
Risultati
|
||||
|
||||
Formazioni
|
||||
|
||||
Andamento set
|
||||
|
||||
Storico match
|
||||
|
||||
Campionato in tempo reale
|
||||
|
||||
Visualizzazione classifica e risultati
|
||||
|
||||
Dati presi direttamente dal sito del CSI
|
||||
|
||||
Aggiornamento automatico o importazione periodica
|
||||
|
||||
Calendario squadra
|
||||
|
||||
Allenamenti
|
||||
|
||||
Partite
|
||||
|
||||
Promemoria
|
||||
|
||||
Vista mensile e lista eventi
|
||||
|
||||
Profilo giocatore
|
||||
|
||||
Foto
|
||||
|
||||
Ruolo
|
||||
|
||||
Statistiche personali
|
||||
|
||||
Badge e obiettivi
|
||||
|
||||
Idea di stile
|
||||
|
||||
Interfaccia sportiva, pulita e moderna
|
||||
|
||||
Molto mobile-first
|
||||
|
||||
Design divertente, energico e più “premium”
|
||||
|
||||
Inserire in seguito il logo della squadra
|
||||
|
||||
Possibile uso di badge, livelli, premi e mini-gamification per rendere l’app più piacevole da usare
|
||||
|
||||
Extra che sarebbe bello aggiungere
|
||||
|
||||
Notifiche push per convocazioni e cambi orario
|
||||
|
||||
Chat o bacheca squadra
|
||||
|
||||
Report automatici dopo le partite
|
||||
|
||||
Sondaggi rapidi per disponibilità
|
||||
|
||||
Sezione “Best of the match”
|
||||
|
||||
Obiettivi di gruppo per presenza e continuità
|
||||
|
||||
Obiettivo finale
|
||||
|
||||
Realizzare una app che non sia solo utile per la gestione della squadra, ma anche piacevole, coinvolgente e bella da usare ogni giorno.
|
||||
|
||||
This project was built with [Lovable](https://lovable.dev).
|
||||
Progetto avviato con [Lovable](https://lovable.dev): i push su `main` sincronizzano l'editor
|
||||
Lovable, quindi si evita di riscrivere la history già pubblicata (niente force-push,
|
||||
rebase/amend/squash di commit già pushati).
|
||||
|
||||
**Live app**: https://volley-cronos-app.lovable.app
|
||||
|
||||
## Build with Lovable
|
||||
## Stack
|
||||
|
||||
Continue developing this project in the [Lovable editor](https://lovable.dev/projects/8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c).
|
||||
- [TanStack Start](https://tanstack.com/start) (React 19, routing file-based) + Vite
|
||||
- Tailwind v4 + componenti shadcn/radix
|
||||
- [Supabase](https://supabase.com) (`@supabase/supabase-js`) per dati e auth
|
||||
- TanStack Query per la cache client
|
||||
|
||||
- **Ship faster**: describe what you want to build and Lovable handles the code.
|
||||
- **Stay in sync**: every change made in Lovable is committed straight to this repository.
|
||||
- **Full ownership**: this code is yours. Push to `main` on GitHub and your changes sync back into Lovable, ready for your next prompt.
|
||||
## Funzionalità
|
||||
|
||||
## Development
|
||||
- **Presenze**: RSVP rapido (presente / assente / forse / in ritardo / infortunato) su ogni evento
|
||||
- **Eventi**: partite, allenamenti, eventi sociali, con calendario e lista
|
||||
- **Scouting live**: registrazione azioni punto-per-punto durante la partita (riservato agli
|
||||
admin)
|
||||
- **Statistiche giocatori**: presenze totali/consecutive, badge, obiettivi squadra, medie pagelle
|
||||
- **Votazioni post-partita**: MVP, pagelle 1-10 tra compagni, voti "social"/goliardici
|
||||
- **Turno palloni**: rotazione automatica di chi porta i palloni, con promemoria push
|
||||
- **Classifica CSI**: al momento un dato demo hardcoded (vedi [CLAUDE.md](CLAUDE.md)), sync reale
|
||||
ancora da implementare
|
||||
- **Notifiche push**: convocazioni, promemoria allenamento/partita, turno palloni, esito finale
|
||||
|
||||
Prefer working locally? You need Node.js and npm — [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
|
||||
## Sviluppo (locale)
|
||||
|
||||
Package manager: **bun** ([installazione](https://bun.sh)).
|
||||
|
||||
```sh
|
||||
git clone <this-repository-url>
|
||||
cd <repository-name>
|
||||
npm i
|
||||
npm run dev
|
||||
git clone https://github.com/ivancacciari1995-a11y/CRAPP.git
|
||||
cd CRAPP
|
||||
bun install
|
||||
```
|
||||
|
||||
### 1. Avvia un backend Supabase
|
||||
|
||||
Serve un'istanza Supabase (cloud o self-hosted) a cui puntare. Per lavorare in locale senza
|
||||
toccare dati reali, nel repo c'è uno stack self-hosted pronto in `infra/supabase/docker/`:
|
||||
|
||||
```sh
|
||||
cd infra/supabase/docker
|
||||
cp .env.example .env
|
||||
sh utils/generate-keys.sh --update-env # genera JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, ecc.
|
||||
```
|
||||
|
||||
Poi avvia lo stack (con `docker-compose.prod.yml` si tengono su solo i servizi che CrAPP usa
|
||||
davvero — vedi la sezione Produzione più sotto):
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
docker compose ps # verifica che tutti i container siano "healthy"
|
||||
```
|
||||
|
||||
Applica le migrazioni del progetto al database appena creato:
|
||||
|
||||
```sh
|
||||
cd ../../.. # torna alla root del repo
|
||||
for f in supabase/migrations/*.sql; do
|
||||
docker exec -i supabase-db psql -U postgres -d postgres -v ON_ERROR_STOP=1 < "$f"
|
||||
done
|
||||
```
|
||||
|
||||
### 2. Configura `.env` di CrAPP
|
||||
|
||||
Nella root del repo, crea `.env` con le chiavi lette da `infra/supabase/docker/.env`:
|
||||
|
||||
```
|
||||
VITE_SUPABASE_URL=http://localhost:8000
|
||||
VITE_SUPABASE_PUBLISHABLE_KEY=<ANON_KEY dello stack self-hosted>
|
||||
SUPABASE_SERVICE_ROLE_KEY=<SERVICE_ROLE_KEY dello stack self-hosted>
|
||||
```
|
||||
|
||||
Facoltative per testare le notifiche push (senza non si rompe nulla, semplicemente niente push):
|
||||
|
||||
```
|
||||
VAPID_PUBLIC_KEY=...
|
||||
VAPID_PRIVATE_KEY=...
|
||||
VAPID_SUBJECT=mailto:tuamail@esempio.it
|
||||
```
|
||||
|
||||
### 3. Avvia l'app
|
||||
|
||||
```sh
|
||||
bun run dev # http://localhost:8080
|
||||
```
|
||||
|
||||
Le tabelle sono vuote al primo avvio: crea un giocatore/evento dall'app stessa per avere dati di
|
||||
test. Dashboard Supabase Studio raggiungibile su `http://localhost:8000` (credenziali
|
||||
`DASHBOARD_USERNAME`/`DASHBOARD_PASSWORD` in `infra/supabase/docker/.env`).
|
||||
|
||||
Altri comandi:
|
||||
|
||||
```sh
|
||||
bun run build:dev # build in modalità development
|
||||
bun run preview # anteprima di una build
|
||||
bun run lint # eslint .
|
||||
bun run format # prettier --write .
|
||||
```
|
||||
|
||||
Non è configurato un test runner.
|
||||
|
||||
## Produzione (self-hosted, Node + Docker)
|
||||
|
||||
`bun run build` usa Nitro (tramite `@lovable.dev/vite-tanstack-config`), che di default
|
||||
compilerebbe per Cloudflare Workers — in [vite.config.ts](vite.config.ts) il preset è forzato a
|
||||
`node-server` per generare un server Node standard, deployabile su qualunque host (coerente con
|
||||
[docs/PORTABILITA.md](docs/PORTABILITA.md)).
|
||||
|
||||
Passo passo, sul server di produzione:
|
||||
|
||||
0. **Dominio/DNS**: un solo hostname pubblico basta — `Caddyfile.example` instrada `/api/*`
|
||||
verso il gateway Supabase (path stripped) e tutto il resto verso l'app, sullo stesso dominio.
|
||||
- Con un dominio vero (DNS proprio): `tuodominio.it`.
|
||||
- Con un dominio **gratuito No-IP** (`ddns.net`, ecc.): un hostname singolo basta e avanza
|
||||
(il piano free ne dà fino a 3, ma qui ne serve uno solo), es. `crapp.ddns.net`.
|
||||
- Serve un client DDNS attivo sul server (o supporto nel router) che aggiorni l'IP: gli
|
||||
hostname No-IP gratuiti scadono dopo ~30 giorni di inattività se non confermati.
|
||||
- Verifica di non essere dietro **CGNAT** (IP pubblico condiviso dall'ISP): in quel caso
|
||||
nessuna porta è raggiungibile da internet e serve un tunnel (es. Cloudflare Tunnel) al
|
||||
posto dell'esposizione diretta.
|
||||
1. **Stack Supabase self-hosted**:
|
||||
```sh
|
||||
cp infra/supabase/docker/.env.production.example infra/supabase/docker/.env
|
||||
cd infra/supabase/docker
|
||||
sh utils/generate-keys.sh --update-env # secret NUOVI, non riusare quelli di sviluppo
|
||||
```
|
||||
Modifica nel `.env` appena creato: `SUPABASE_PUBLIC_URL`/`API_EXTERNAL_URL` (dominio del
|
||||
punto 0 + `/api`), `SITE_URL` (dominio del punto 0), `DASHBOARD_PASSWORD`, e l'SMTP se
|
||||
servono email vere. Poi avvia:
|
||||
```sh
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
```
|
||||
`docker-compose.prod.yml` fa due cose, pensate anche per girare su hardware limitato (es.
|
||||
Raspberry Pi 4): non espone porte pubblicamente (solo `127.0.0.1`, dietro il reverse proxy —
|
||||
vedi `Caddyfile.example` nella stessa cartella), e disattiva i servizi Supabase che CrAPP non
|
||||
usa (Realtime, Storage, imgproxy, Edge Functions, pooler) — restano solo `db`, `auth`, `rest`,
|
||||
`api-gw`, più `studio`+`meta` per guardare/gestire il database via interfaccia grafica
|
||||
(raggiungibile su `<dominio>/api`, con le credenziali `DASHBOARD_USERNAME`/
|
||||
`DASHBOARD_PASSWORD`). Da 11 container si scende a 6.
|
||||
2. **Migrazioni**:
|
||||
```sh
|
||||
cd ../.. # root del repo
|
||||
for f in supabase/migrations/*.sql; do
|
||||
docker exec -i supabase-db psql -U postgres -d postgres -v ON_ERROR_STOP=1 < "$f"
|
||||
done
|
||||
```
|
||||
3. **Stack Strapi** (CMS admin per rosa, classifica, storico partite, scout finalizzato — vedi
|
||||
`infra/strapi/`): usa lo stesso Postgres dello stack Supabase, in un database logico separato:
|
||||
```sh
|
||||
docker exec -i supabase-db psql -U postgres -c "CREATE DATABASE strapi;"
|
||||
docker exec -i supabase-db psql -U postgres -c "CREATE USER strapi WITH PASSWORD '...';"
|
||||
docker exec -i supabase-db psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE strapi TO strapi;"
|
||||
cp infra/strapi/.env.production.example infra/strapi/.env
|
||||
```
|
||||
Compila in `infra/strapi/.env` i secret (`APP_KEYS`, `JWT_SECRET`, ecc. — genera ognuno con
|
||||
`openssl rand -base64 32`) e `DATABASE_PASSWORD` (la stessa scelta sopra), poi:
|
||||
```sh
|
||||
cd infra/strapi
|
||||
docker compose up -d --build
|
||||
```
|
||||
Al primo accesso su `<dominio>/cms/admin` crea l'utente amministratore Strapi. Da lì, un solo
|
||||
giro di setup manuale (nessuna automazione: i permessi Strapi si abilitano solo dall'admin UI):
|
||||
in Settings → Users & Permissions Plugin → Roles → Public abilita `find`/`findOne` per
|
||||
`Giocatore`, `Riga-classifica` e `Match-storico`; poi in Settings → API Tokens genera un token
|
||||
con permesso `create` solo su `Scout-match-finale` (va in `STRAPI_WRITE_TOKEN` nell'env
|
||||
dell'app, punto 5). Infine inserisci a mano rosa/classifica/storico nelle rispettive collezioni
|
||||
(i valori di partenza sono nella cronologia git di `src/lib/crapp-data.ts`, prima che venissero
|
||||
spostati qui).
|
||||
4. **Reverse proxy TLS**: copia `infra/supabase/docker/Caddyfile.example` in `Caddyfile`,
|
||||
sostituisci il dominio del punto 0, poi `caddy run --config Caddyfile` (o come container).
|
||||
Gestisce automaticamente il certificato Let's Encrypt.
|
||||
5. **Env dell'app**:
|
||||
```sh
|
||||
cp .env.production.example .env
|
||||
```
|
||||
Compila `VITE_SUPABASE_URL` (dominio del punto 0 + `/api`),
|
||||
`VITE_SUPABASE_PUBLISHABLE_KEY` (= `ANON_KEY` dello stack), `SUPABASE_SERVICE_ROLE_KEY`
|
||||
(= `SERVICE_ROLE_KEY`), `VITE_STRAPI_URL` (dominio del punto 0 + `/cms`), `STRAPI_WRITE_TOKEN`
|
||||
(il token generato al punto 3), e le chiavi `VAPID_*` reali
|
||||
(`npx web-push generate-vapid-keys`).
|
||||
6. **Build e avvio**:
|
||||
```sh
|
||||
bun run build
|
||||
node .output/server/index.mjs # ascolta su PORT (default 3000)
|
||||
```
|
||||
Tienilo vivo con un process manager (pm2/systemd) — il Caddy del punto 4 lo espone via TLS
|
||||
sull'hostname app.
|
||||
7. **Job pianificati** — gli endpoint `src/routes/api/public/promemoria-palloni.ts` e
|
||||
`sollecita-presenze.ts` vanno richiamati periodicamente via HTTP POST da un cron di sistema o
|
||||
`pg_cron`: non partono da soli.
|
||||
|
||||
## Continuare da Lovable
|
||||
|
||||
Il progetto resta modificabile anche dall'[editor Lovable](https://lovable.dev/projects/8d07b0e4-6bd2-4a17-9dd2-bb2cf13f9f7c):
|
||||
le modifiche fatte lì vengono committate direttamente su questo repository, e viceversa i push
|
||||
su `main` sincronizzano l'editor.
|
||||
|
||||
## Portabilità
|
||||
|
||||
Il progetto è pensato per restare deployabile su un normale server **Node.js + PostgreSQL**,
|
||||
senza dipendenze esclusive da Lovable Cloud — vedi [docs/PORTABILITA.md](docs/PORTABILITA.md)
|
||||
per lo stato attuale e le regole da rispettare.
|
||||
|
||||
## Note per chi sviluppa con Claude Code
|
||||
|
||||
Vedi [CLAUDE.md](CLAUDE.md) per architettura dettagliata, convenzioni del repo e vincoli di
|
||||
efficienza sul piano cloud a consumo.
|
||||
|
||||
@@ -5,34 +5,6 @@
|
||||
"": {
|
||||
"name": "tanstack_start_ts",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@lovable.dev/cloud-auth-js": "^1.1.2",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-context-menu": "^2.2.16",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-menubar": "^1.1.16",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@supabase/supabase-js": "^2.111.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/react-query": "^5.101.1",
|
||||
@@ -40,19 +12,10 @@
|
||||
"@tanstack/react-start": "^1.168.32",
|
||||
"@tanstack/router-plugin": "^1.168.23",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.575.0",
|
||||
"react": "^19.2.0",
|
||||
"react-day-picker": "^9.14.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-hook-form": "^7.71.2",
|
||||
"react-resizable-panels": "^4.6.5",
|
||||
"recharts": "^2.15.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
@@ -116,16 +79,12 @@
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
|
||||
|
||||
"@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
@@ -150,16 +109,6 @@
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="],
|
||||
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="],
|
||||
|
||||
"@hookform/resolvers": ["@hookform/resolvers@5.5.7", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "@sinclair/typebox": ">=0.25.24", "@standard-schema/spec": "^1.0.0", "@typeschema/main": ">=0.13.7", "@vinejs/vine": "^2.0.0 || ^3.0.0", "ajv": "^8.12.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "arktype": "^2.0.0", "ata-validator": "^0.7.0", "class-transformer": ">=0.4.0", "class-validator": ">=0.12.0", "computed-types": "^1.0.0", "effect": "^3.10.3", "fluentvalidation-ts": "^3.0.0", "fp-ts": "^2.7.0", "io-ts": "^2.0.0", "joi": "^17.0.0", "nope-validator": ">=0.12.0", "react-hook-form": "^7.55.0", "superstruct": ">=0.12.0", "typanion": "^3.3.2", "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", "vest": ">=3.0.0", "yup": "^1.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@sinclair/typebox", "@standard-schema/spec", "@typeschema/main", "@vinejs/vine", "ajv", "ajv-errors", "ajv-formats", "arktype", "ata-validator", "class-transformer", "class-validator", "computed-types", "effect", "fluentvalidation-ts", "fp-ts", "io-ts", "joi", "nope-validator", "superstruct", "typanion", "valibot", "vest", "yup", "zod"] }, "sha512-CyPCYV8/KlXfEXLWj8HHHhVsR/IZ6Ckm3b/a4fWtO/lRnRK1huqncb8LlAWrmpPRsse5glF5aVuDRMHEr3UGag=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
|
||||
@@ -180,8 +129,6 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@lovable.dev/cloud-auth-js": ["@lovable.dev/cloud-auth-js@1.1.2", "https://europe-west4-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@lovable.dev/cloud-auth-js/-/cloud-auth-js-1.1.2.tgz", {}, "sha512-xz8ocewsgwkp8giau272/eWWU3XrchCg5uba4yQPPYtevHTXaVU3sD+fO1JjyPBHacVcOcwhmgUiU9TKHt63cg=="],
|
||||
|
||||
"@lovable.dev/vite-plugin-dev-server-bridge": ["@lovable.dev/vite-plugin-dev-server-bridge@1.2.1", "", { "peerDependencies": { "vite": ">=5.0.0 <9.0.0" } }, "sha512-JdADRwpEJA5t0ggXbd96dND23Wsp69Af9lVSV5m7MnMGxJZlPgqAqz7HUKcfJESfI5M5yFZOl9e0x76JMMDOAg=="],
|
||||
|
||||
"@lovable.dev/vite-plugin-hmr-gate": ["@lovable.dev/vite-plugin-hmr-gate@1.3.5", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@lovable.dev/vite-plugin-hmr-gate/-/vite-plugin-hmr-gate-1.3.5.tgz", { "peerDependencies": { "vite": ">=5.0.0 <9.0.0" } }, "sha512-LxCj6JIbYRQ6peMm2aVn/bhHLkwZ5eZ4Cn6gmh7LfjaplqZHlA24nhCowgt6ZLfbckc4d8tSy0He7H8EiFEXag=="],
|
||||
@@ -242,112 +189,38 @@
|
||||
|
||||
"@pkgr/core": ["@pkgr/core@0.3.6", "", {}, "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA=="],
|
||||
|
||||
"@radix-ui/number": ["@radix-ui/number@1.1.3", "", {}, "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.7", "", {}, "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q=="],
|
||||
|
||||
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collapsible": "1.1.20", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg=="],
|
||||
|
||||
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dialog": "1.1.23", "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VAYOiQRqj3GPpYJE0I9J+X8Ip05cyVlNdKOFeiGS2Ou1HHGfpl0BxOyZm6nmVDyU+W+NF3/XLzmjHmVGydhwgA=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA=="],
|
||||
|
||||
"@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-fy+dyVR+90nelK8rqIznFlxzx7uPcGbhxH8Nfr2bHb4UfSe+e3hklOC0luK0hDwVwnRX7xTRySpsrQVeW+/oNQ=="],
|
||||
|
||||
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA=="],
|
||||
|
||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ=="],
|
||||
|
||||
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.20", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q=="],
|
||||
|
||||
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.15", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.5", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA=="],
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.2.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA=="],
|
||||
|
||||
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew=="],
|
||||
|
||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA=="],
|
||||
|
||||
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-effect-event": "0.0.5" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w=="],
|
||||
|
||||
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.24", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g=="],
|
||||
|
||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.6", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ=="],
|
||||
|
||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.16", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ=="],
|
||||
|
||||
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ=="],
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA=="],
|
||||
|
||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g=="],
|
||||
|
||||
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.24", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-callback-ref": "1.1.4", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA=="],
|
||||
|
||||
"@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.24", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-menu": "2.1.24", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-eeVs0vf7cuqXaM0qLQCPcufImiJNVBXdJDLu7ZGYl2732UH23Qat/foNGrr6vYV3/DdTsBqASoggUFgH14OcZA=="],
|
||||
|
||||
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.22", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-previous": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ou7iLEJ+yrhQndkkA4U21XIdS/CS45F4iXIkTZcb6/Ne9EMsOuDudVmCwmDnfFZZ+y1FZqXRNSIgBy+YMvZVZg=="],
|
||||
|
||||
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.23", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ=="],
|
||||
|
||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.3.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-rect": "1.1.4", "@radix-ui/react-use-size": "1.1.4", "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg=="],
|
||||
|
||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.17", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ=="],
|
||||
|
||||
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.10", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.10", "", { "dependencies": { "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg=="],
|
||||
|
||||
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.16", "", { "dependencies": { "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg=="],
|
||||
|
||||
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.4.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ=="],
|
||||
|
||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-is-hydrated": "0.1.3", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ=="],
|
||||
|
||||
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.18", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA=="],
|
||||
|
||||
"@radix-ui/react-select": ["@radix-ui/react-select@2.3.7", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-focus-guards": "1.1.6", "@radix-ui/react-focus-scope": "1.1.16", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-callback-ref": "1.1.4", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-previous": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg=="],
|
||||
|
||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.15", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw=="],
|
||||
|
||||
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.4.7", "", { "dependencies": { "@radix-ui/number": "1.1.3", "@radix-ui/primitive": "1.1.7", "@radix-ui/react-collection": "1.1.15", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-use-previous": "1.1.4", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q=="],
|
||||
|
||||
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.3.7", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-size": "1.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw=="],
|
||||
|
||||
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.21", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog=="],
|
||||
|
||||
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.18", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug=="],
|
||||
|
||||
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-direction": "1.1.4", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-roving-focus": "1.1.19", "@radix-ui/react-toggle": "1.1.18", "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA=="],
|
||||
|
||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-compose-refs": "1.1.5", "@radix-ui/react-context": "1.2.2", "@radix-ui/react-dismissable-layer": "1.1.19", "@radix-ui/react-id": "1.1.4", "@radix-ui/react-popper": "1.3.7", "@radix-ui/react-portal": "1.1.17", "@radix-ui/react-presence": "1.1.10", "@radix-ui/react-primitive": "2.1.10", "@radix-ui/react-slot": "1.3.3", "@radix-ui/react-use-controllable-state": "1.2.6", "@radix-ui/react-use-layout-effect": "1.1.4", "@radix-ui/react-visually-hidden": "1.2.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg=="],
|
||||
|
||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ=="],
|
||||
|
||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.7", "@radix-ui/react-use-effect-event": "0.0.5", "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ=="],
|
||||
|
||||
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.5", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg=="],
|
||||
|
||||
"@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw=="],
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw=="],
|
||||
|
||||
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg=="],
|
||||
|
||||
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.4", "", { "dependencies": { "@radix-ui/rect": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ=="],
|
||||
|
||||
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.4", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw=="],
|
||||
|
||||
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.11", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ=="],
|
||||
|
||||
"@radix-ui/rect": ["@radix-ui/rect@1.1.3", "", {}, "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9yB1l95IrJuNGDFdOYe79vdApdz6WWBCObE+rQ2LUliYUlcyFwSYIb2xb5/Ifw7dAtMy2ZqNyd8QTSOc7duAKw=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pexNaW9ACLUOaBITOpU6qVu4VrsOFIjTv6bzgu0YUATo4eUJx0V605PxwZfndpPOn0ilqGqvGQ0M8UW0IE24jg=="],
|
||||
@@ -380,8 +253,6 @@
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@supabase/auth-js": ["@supabase/auth-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/auth-js/-/auth-js-2.111.0.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-hbRLgyQZEX0SDyF4LYXpv94qOIQyFATfpT5SIs2V0SisHnisVRkYgiPQWzv80l4S2mO3qof78rmoH9j5zoFsYQ=="],
|
||||
|
||||
"@supabase/functions-js": ["@supabase/functions-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/functions-js/-/functions-js-2.111.0.tgz", { "dependencies": { "tslib": "2.8.1" } }, "sha512-RW/OCsd6MO592zU8ifzP8/f8XzxxIdpb+Up5XaOtE26Fw+3zTp475WX7+GuuktiD1WF8pFUDe6khUPbMp77RCw=="],
|
||||
@@ -396,8 +267,6 @@
|
||||
|
||||
"@supabase/supabase-js": ["@supabase/supabase-js@2.111.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@supabase/supabase-js/-/supabase-js-2.111.0.tgz", { "dependencies": { "@supabase/auth-js": "2.111.0", "@supabase/functions-js": "2.111.0", "@supabase/postgrest-js": "2.111.0", "@supabase/realtime-js": "2.111.0", "@supabase/storage-js": "2.111.0" } }, "sha512-9q0/AULthQnWeiDh1vGyjoJZbSY04bu6qHcWit70pqEYn5Kv/dkCPY62Ja1123jEnJbB9Vd2pjY7Kvk/lK3peA=="],
|
||||
|
||||
"@tabby_ai/hijri-converter": ["@tabby_ai/hijri-converter@1.0.5", "", {}, "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="],
|
||||
@@ -490,24 +359,6 @@
|
||||
|
||||
"@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "https://europe-west1-npm.pkg.dev/lovable-core-prod/sandbox-npm-cache/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
|
||||
|
||||
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
@@ -574,12 +425,8 @@
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
@@ -598,38 +445,10 @@
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||
|
||||
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="],
|
||||
|
||||
"date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
|
||||
|
||||
"db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
@@ -638,16 +457,8 @@
|
||||
|
||||
"diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
|
||||
|
||||
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.398", "", {}, "sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ=="],
|
||||
|
||||
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
|
||||
|
||||
"embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="],
|
||||
|
||||
"embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ=="],
|
||||
|
||||
"env-runner": ["env-runner@0.1.16", "", { "dependencies": { "crossws": "^0.4.8", "exsolve": "^1.1.0", "httpxy": "^0.5.4", "srvx": "^0.11.19" }, "peerDependencies": { "@netlify/runtime": "^4.1.23", "@vercel/queue": ">=0.2.0", "miniflare": "^4.20260515.0", "wrangler": "^4.0.0" }, "optionalPeers": ["@netlify/runtime", "@vercel/queue", "miniflare", "wrangler"], "bin": { "env-runner": "dist/cli.mjs" } }, "sha512-2LRJM4P2KLX6J83QZZrMqvgCDt/D5ea7wPcI3yYiy5cG/9rX5QwdwZFx0D7ktWnjdRyZxYjttGGorb5nFqb1CA=="],
|
||||
@@ -680,16 +491,12 @@
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||
|
||||
"exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
|
||||
|
||||
"fast-equals": ["fast-equals@5.4.1", "", {}, "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
@@ -738,10 +545,6 @@
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
@@ -798,12 +601,8 @@
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.575.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg=="],
|
||||
@@ -824,8 +623,6 @@
|
||||
|
||||
"node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"ocache": ["ocache@0.1.5", "", { "dependencies": { "ohash": "^2.0.11" } }, "sha512-kNNnkkVQup/QDvmTz8Q84wc2ntiyoVHDxa6eHWKt5qdGAmFRBIxy83rxgCYEjW0x06UJ9E3P6VgM2yY4rOBH4w=="],
|
||||
|
||||
"ofetch": ["ofetch@2.0.0-alpha.3", "", {}, "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA=="],
|
||||
@@ -860,40 +657,22 @@
|
||||
|
||||
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||
|
||||
"react-day-picker": ["react-day-picker@9.14.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "@tabby_ai/hijri-converter": "1.0.5", "date-fns": "^4.1.0", "date-fns-jalali": "4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.83.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-AXt8cMCmx5a7u4uvpb2uRFVrWQhllI4pV+LSykxIac/hjt44TnQkmX9BKuQi2i+LDC62esmiLpilkav+kjVf/A=="],
|
||||
|
||||
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-resizable-panels": ["react-resizable-panels@4.12.2", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q=="],
|
||||
|
||||
"react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="],
|
||||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
|
||||
|
||||
"recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"rolldown": ["rolldown@1.2.0", "", { "dependencies": { "@oxc-project/types": "=0.140.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.0", "@rolldown/binding-darwin-arm64": "1.2.0", "@rolldown/binding-darwin-x64": "1.2.0", "@rolldown/binding-freebsd-x64": "1.2.0", "@rolldown/binding-linux-arm-gnueabihf": "1.2.0", "@rolldown/binding-linux-arm64-gnu": "1.2.0", "@rolldown/binding-linux-arm64-musl": "1.2.0", "@rolldown/binding-linux-ppc64-gnu": "1.2.0", "@rolldown/binding-linux-s390x-gnu": "1.2.0", "@rolldown/binding-linux-x64-gnu": "1.2.0", "@rolldown/binding-linux-x64-musl": "1.2.0", "@rolldown/binding-openharmony-arm64": "1.2.0", "@rolldown/binding-wasm32-wasi": "1.2.0", "@rolldown/binding-win32-arm64-msvc": "1.2.0", "@rolldown/binding-win32-x64-msvc": "1.2.0" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-u7tgm5l4Yw1iTqUL4EcYOAt7fFvCgQMLeidrnD4GALlC6aOznCjezYajgxeyKw27u0Q5N7fwgCzjVyPIWzwuBA=="],
|
||||
@@ -934,8 +713,6 @@
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
|
||||
@@ -974,8 +751,6 @@
|
||||
|
||||
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
||||
|
||||
"victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="],
|
||||
|
||||
"vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
|
||||
|
||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
||||
@@ -1042,8 +817,6 @@
|
||||
|
||||
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="],
|
||||
|
||||
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"vite/rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
|
||||
|
||||
@@ -15,6 +15,7 @@ senza dipendere da servizi esclusivi di Lovable Cloud.
|
||||
| Auth | Sì | GoTrue self-hosted oppure qualsiasi provider OIDC. |
|
||||
| `src/integrations/lovable/*` | Opzionale | Login social gestito da Lovable: **non importato da nessuna schermata**, rimovibile senza impatti. |
|
||||
| `@lovable.dev/vite-tanstack-config` | Solo build | Preset Vite; sostituibile con una config Vite/TanStack esplicita. |
|
||||
| CMS Strapi (`infra/strapi/`) | Sì | Open source e self-hostable, Postgres come backend; l'app lo consuma solo via `src/lib/strapi-client.ts`. |
|
||||
|
||||
## Regole da rispettare nelle prossime modifiche
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
HOST=0.0.0.0
|
||||
PORT=1337
|
||||
APP_KEYS="toBeModified1,toBeModified2"
|
||||
API_TOKEN_SALT=tobemodified
|
||||
ADMIN_JWT_SECRET=tobemodified
|
||||
TRANSFER_TOKEN_SALT=tobemodified
|
||||
JWT_SECRET=tobemodified
|
||||
ENCRYPTION_KEY=tobemodified
|
||||
@@ -0,0 +1,23 @@
|
||||
# TEMPLATE DI PRODUZIONE per il CMS Strapi (rosa, classifica, storico partite, scout finalizzato).
|
||||
# Copia in .env accanto a docker-compose.yml e compila. Rigenera SEMPRE secret nuovi in produzione,
|
||||
# non riusare quelli di sviluppo — un valore per riga, generabili con: openssl rand -base64 32
|
||||
|
||||
HOST=0.0.0.0
|
||||
PORT=1337
|
||||
|
||||
APP_KEYS=
|
||||
API_TOKEN_SALT=
|
||||
ADMIN_JWT_SECRET=
|
||||
JWT_SECRET=
|
||||
TRANSFER_TOKEN_SALT=
|
||||
ENCRYPTION_KEY=
|
||||
|
||||
# Stesso cluster Postgres dello stack Supabase self-hosted (infra/supabase/docker/), database
|
||||
# logico separato — vedi README "Produzione", passo Strapi.
|
||||
DATABASE_CLIENT=postgres
|
||||
DATABASE_HOST=db
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_NAME=strapi
|
||||
DATABASE_USERNAME=strapi
|
||||
DATABASE_PASSWORD=
|
||||
DATABASE_SSL=false
|
||||
@@ -0,0 +1,131 @@
|
||||
############################
|
||||
# OS X
|
||||
############################
|
||||
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
._*
|
||||
|
||||
|
||||
############################
|
||||
# Linux
|
||||
############################
|
||||
|
||||
*~
|
||||
|
||||
|
||||
############################
|
||||
# Windows
|
||||
############################
|
||||
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
|
||||
############################
|
||||
# Packages
|
||||
############################
|
||||
|
||||
*.7z
|
||||
*.csv
|
||||
*.dat
|
||||
*.dmg
|
||||
*.gz
|
||||
*.iso
|
||||
*.jar
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.o
|
||||
*.seed
|
||||
*.so
|
||||
*.swo
|
||||
*.swp
|
||||
*.swn
|
||||
*.swm
|
||||
*.out
|
||||
*.pid
|
||||
|
||||
|
||||
############################
|
||||
# Logs and databases
|
||||
############################
|
||||
|
||||
.tmp
|
||||
*.log
|
||||
*.sql
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
|
||||
############################
|
||||
# Misc.
|
||||
############################
|
||||
|
||||
*#
|
||||
ssl
|
||||
.idea
|
||||
nbproject
|
||||
public/uploads/*
|
||||
!public/uploads/.gitkeep
|
||||
.tsbuildinfo
|
||||
.eslintcache
|
||||
|
||||
############################
|
||||
# Node.js
|
||||
############################
|
||||
|
||||
lib-cov
|
||||
lcov.info
|
||||
pids
|
||||
logs
|
||||
results
|
||||
node_modules
|
||||
.node_history
|
||||
|
||||
############################
|
||||
# Package managers
|
||||
############################
|
||||
|
||||
.yarn/*
|
||||
!.yarn/cache
|
||||
!.yarn/unplugged
|
||||
!.yarn/patches
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
yarn-error.log
|
||||
|
||||
############################
|
||||
# Tests
|
||||
############################
|
||||
|
||||
coverage
|
||||
|
||||
############################
|
||||
# Strapi
|
||||
############################
|
||||
|
||||
.env
|
||||
license.txt
|
||||
exports
|
||||
.strapi
|
||||
dist
|
||||
build
|
||||
.strapi-updater.json
|
||||
.strapi-cloud.json
|
||||
@@ -0,0 +1,15 @@
|
||||
# Immagine di produzione per il CMS admin (rosa, classifica, storico partite, scout finalizzato).
|
||||
# Build multi-stage: installa e builda l'admin panel, poi copia solo l'output nell'immagine finale.
|
||||
FROM node:20-slim AS build
|
||||
WORKDIR /opt/app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-slim
|
||||
WORKDIR /opt/app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=build /opt/app .
|
||||
EXPOSE 1337
|
||||
CMD ["npm", "run", "start"]
|
||||
@@ -0,0 +1,61 @@
|
||||
# 🚀 Getting started with Strapi
|
||||
|
||||
Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/dev-docs/cli) (CLI) which lets you scaffold and manage your project in seconds.
|
||||
|
||||
### `develop`
|
||||
|
||||
Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-develop)
|
||||
|
||||
```
|
||||
npm run develop
|
||||
# or
|
||||
yarn develop
|
||||
```
|
||||
|
||||
### `start`
|
||||
|
||||
Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-start)
|
||||
|
||||
```
|
||||
npm run start
|
||||
# or
|
||||
yarn start
|
||||
```
|
||||
|
||||
### `build`
|
||||
|
||||
Build your admin panel. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-build)
|
||||
|
||||
```
|
||||
npm run build
|
||||
# or
|
||||
yarn build
|
||||
```
|
||||
|
||||
## ⚙️ Deployment
|
||||
|
||||
Strapi gives you many possible deployment options for your project including [Strapi Cloud](https://cloud.strapi.io). Browse the [deployment section of the documentation](https://docs.strapi.io/dev-docs/deployment) to find the best solution for your use case.
|
||||
|
||||
```
|
||||
yarn strapi deploy
|
||||
```
|
||||
|
||||
## 📚 Learn more
|
||||
|
||||
- [Resource center](https://strapi.io/resource-center) - Strapi resource center.
|
||||
- [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation.
|
||||
- [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community.
|
||||
- [Strapi blog](https://strapi.io/blog) - Official Strapi blog containing articles made by the Strapi team and the community.
|
||||
- [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements.
|
||||
|
||||
Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome!
|
||||
|
||||
## ✨ Community
|
||||
|
||||
- [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team.
|
||||
- [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members.
|
||||
- [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi.
|
||||
|
||||
---
|
||||
|
||||
<sub>🤫 Psst! [Strapi is hiring](https://strapi.io/careers).</sub>
|
||||
@@ -0,0 +1,21 @@
|
||||
module.exports = ({ env }) => ({
|
||||
auth: {
|
||||
secret: env('ADMIN_JWT_SECRET'),
|
||||
},
|
||||
apiToken: {
|
||||
salt: env('API_TOKEN_SALT'),
|
||||
},
|
||||
transfer: {
|
||||
token: {
|
||||
salt: env('TRANSFER_TOKEN_SALT'),
|
||||
},
|
||||
},
|
||||
secrets: {
|
||||
encryptionKey: env('ENCRYPTION_KEY'),
|
||||
},
|
||||
flags: {
|
||||
nps: env.bool('FLAG_NPS', true),
|
||||
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
|
||||
docLinks: env.bool('FLAG_DOC_LINKS', true),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
rest: {
|
||||
defaultLimit: 25,
|
||||
maxLimit: 100,
|
||||
withCount: true,
|
||||
strictParams: true,
|
||||
},
|
||||
documents: {
|
||||
strictParams: true,
|
||||
strictRelations: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/** @import { Core } from '@strapi/strapi' */
|
||||
|
||||
const path = require('path');
|
||||
const { isDatabaseClientKind } = require('@strapi/database');
|
||||
|
||||
module.exports = ({ env }) => {
|
||||
const client = env('DATABASE_CLIENT', 'sqlite');
|
||||
|
||||
if (!isDatabaseClientKind(client)) {
|
||||
throw new Error(
|
||||
`Unsupported DATABASE_CLIENT: ${client}. Use "postgres", "mysql", or "sqlite".`
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {Record<Core.Config.Database.ClientKind, Core.Config.Database['connection']>} */
|
||||
const connections = {
|
||||
mysql: {
|
||||
client: 'mysql',
|
||||
connection: {
|
||||
host: env('DATABASE_HOST', 'localhost'),
|
||||
port: env.int('DATABASE_PORT', 3306),
|
||||
database: env('DATABASE_NAME', 'strapi'),
|
||||
user: env('DATABASE_USERNAME', 'strapi'),
|
||||
password: env('DATABASE_PASSWORD', 'strapi'),
|
||||
ssl: env.bool('DATABASE_SSL', false) && {
|
||||
key: env('DATABASE_SSL_KEY', undefined),
|
||||
cert: env('DATABASE_SSL_CERT', undefined),
|
||||
ca: env('DATABASE_SSL_CA', undefined),
|
||||
capath: env('DATABASE_SSL_CAPATH', undefined),
|
||||
cipher: env('DATABASE_SSL_CIPHER', undefined),
|
||||
rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
|
||||
},
|
||||
},
|
||||
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
|
||||
},
|
||||
postgres: {
|
||||
client: 'postgres',
|
||||
connection: {
|
||||
connectionString: env('DATABASE_URL'),
|
||||
host: env('DATABASE_HOST', 'localhost'),
|
||||
port: env.int('DATABASE_PORT', 5432),
|
||||
database: env('DATABASE_NAME', 'strapi'),
|
||||
user: env('DATABASE_USERNAME', 'strapi'),
|
||||
password: env('DATABASE_PASSWORD', 'strapi'),
|
||||
ssl: env.bool('DATABASE_SSL', false) && {
|
||||
key: env('DATABASE_SSL_KEY', undefined),
|
||||
cert: env('DATABASE_SSL_CERT', undefined),
|
||||
ca: env('DATABASE_SSL_CA', undefined),
|
||||
capath: env('DATABASE_SSL_CAPATH', undefined),
|
||||
cipher: env('DATABASE_SSL_CIPHER', undefined),
|
||||
rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
|
||||
},
|
||||
schema: env('DATABASE_SCHEMA', 'public'),
|
||||
},
|
||||
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
|
||||
},
|
||||
sqlite: {
|
||||
client: 'sqlite',
|
||||
connection: {
|
||||
filename: path.join(__dirname, '..', env('DATABASE_FILENAME', '.tmp/data.db')),
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
connection: {
|
||||
...connections[client],
|
||||
acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = [
|
||||
'strapi::logger',
|
||||
'strapi::errors',
|
||||
'strapi::security',
|
||||
'strapi::cors',
|
||||
'strapi::poweredBy',
|
||||
'strapi::query',
|
||||
'strapi::body',
|
||||
'strapi::session',
|
||||
'strapi::favicon',
|
||||
'strapi::public',
|
||||
];
|
||||
@@ -0,0 +1,41 @@
|
||||
const allowedMediaTypes = [
|
||||
'image/*',
|
||||
'video/*',
|
||||
'audio/*',
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.*',
|
||||
'text/plain',
|
||||
'text/csv',
|
||||
];
|
||||
|
||||
const deniedTypes = [
|
||||
'image/svg+xml',
|
||||
'application/vnd.microsoft.portable-executable',
|
||||
'application/x-msdownload',
|
||||
'application/x-msdos-program',
|
||||
'application/x-executable',
|
||||
'application/x-dosexec',
|
||||
'application/x-sh',
|
||||
'text/x-shellscript',
|
||||
'application/x-mach-binary',
|
||||
];
|
||||
|
||||
module.exports = () => ({
|
||||
'users-permissions': {
|
||||
config: {
|
||||
jwtManagement: 'refresh',
|
||||
sessions: {
|
||||
httpOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
upload: {
|
||||
config: {
|
||||
security: {
|
||||
allowedTypes: allowedMediaTypes,
|
||||
deniedTypes,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
module.exports = ({ env }) => ({
|
||||
host: env('HOST', '0.0.0.0'),
|
||||
port: env.int('PORT', 1337),
|
||||
app: {
|
||||
keys: env.array('APP_KEYS'),
|
||||
},
|
||||
webhooks: {
|
||||
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
# CMS admin (rosa, classifica, storico partite, scout finalizzato) per CrAPP.
|
||||
# Si appoggia allo stesso Postgres dello stack Supabase self-hosted (infra/supabase/docker/,
|
||||
# progetto compose "supabase", rete di default "supabase_default" — verifica con
|
||||
# `docker network ls` se il nome differisce) con un database logico separato ("strapi"), invece
|
||||
# di un secondo container Postgres: stesso isolamento dei dati, metà del carico su hardware
|
||||
# limitato (es. Raspberry Pi 4).
|
||||
#
|
||||
# Uso:
|
||||
# cp .env.production.example .env # compila i secret, vedi README "Produzione"
|
||||
# docker compose up -d
|
||||
#
|
||||
# Crea prima il database logico (una tantum, vedi README):
|
||||
# docker exec -i supabase-db psql -U postgres -c "CREATE DATABASE strapi;"
|
||||
# docker exec -i supabase-db psql -U postgres -c "CREATE USER strapi WITH PASSWORD '...';"
|
||||
# docker exec -i supabase-db psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE strapi TO strapi;"
|
||||
|
||||
services:
|
||||
strapi:
|
||||
build: .
|
||||
container_name: crapp-strapi
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
# Solo locale, come api-gw dello stack Supabase — dietro il reverse proxy TLS (Caddyfile).
|
||||
- "127.0.0.1:1337:1337"
|
||||
volumes:
|
||||
- strapi-uploads:/opt/app/public/uploads
|
||||
networks:
|
||||
- supabase
|
||||
|
||||
networks:
|
||||
supabase:
|
||||
name: supabase_default
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
strapi-uploads:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 497 B |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"target": "ES2021",
|
||||
"checkJs": true,
|
||||
"allowJs": true
|
||||
}
|
||||
}
|
||||
Generated
+21579
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "strapi",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "A Strapi application",
|
||||
"scripts": {
|
||||
"build": "strapi build",
|
||||
"console": "strapi console",
|
||||
"deploy": "strapi deploy",
|
||||
"dev": "strapi develop",
|
||||
"develop": "strapi develop",
|
||||
"start": "strapi start",
|
||||
"strapi": "strapi",
|
||||
"upgrade": "npx @strapi/upgrade latest",
|
||||
"upgrade:dry": "npx @strapi/upgrade latest --dry"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strapi/database": "5.52.2",
|
||||
"@strapi/plugin-cloud": "5.52.2",
|
||||
"@strapi/plugin-users-permissions": "5.52.2",
|
||||
"@strapi/strapi": "5.52.2",
|
||||
"pg": "8.20.0",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"styled-components": "^6.0.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"engines": {
|
||||
"node": ">=20.0.0 <=26.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"strapi": {
|
||||
"uuid": "7cc5aaf5-d55f-4ee8-9141-02e2dd523a1c",
|
||||
"installId": "8e493229589483c80fc99464f570c7c84540d2c64f9fe878c9677834e48e6fa1"
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild@0.28.2": true,
|
||||
"esbuild@0.21.5": true,
|
||||
"@swc/core@1.16.1": true,
|
||||
"core-js-pure@3.50.0": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# To prevent search engines from seeing the site altogether, uncomment the next two lines:
|
||||
# User-Agent: *
|
||||
# Disallow: /
|
||||
@@ -0,0 +1,39 @@
|
||||
const config = {
|
||||
locales: [
|
||||
// 'ar',
|
||||
// 'fr',
|
||||
// 'cs',
|
||||
// 'de',
|
||||
// 'da',
|
||||
// 'es',
|
||||
// 'he',
|
||||
// 'id',
|
||||
// 'it',
|
||||
// 'ja',
|
||||
// 'ko',
|
||||
// 'ms',
|
||||
// 'nl',
|
||||
// 'no',
|
||||
// 'pl',
|
||||
// 'pt-BR',
|
||||
// 'pt',
|
||||
// 'ru',
|
||||
// 'sk',
|
||||
// 'sv',
|
||||
// 'th',
|
||||
// 'tr',
|
||||
// 'uk',
|
||||
// 'vi',
|
||||
// 'zh-Hans',
|
||||
// 'zh',
|
||||
],
|
||||
};
|
||||
|
||||
const bootstrap = (app) => {
|
||||
console.log(app);
|
||||
};
|
||||
|
||||
export default {
|
||||
config,
|
||||
bootstrap,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
const { mergeConfig } = require('vite');
|
||||
|
||||
module.exports = (config) => {
|
||||
// Important: always return the modified config
|
||||
return mergeConfig(config, {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src',
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "giocatori",
|
||||
"info": {
|
||||
"singularName": "giocatore",
|
||||
"pluralName": "giocatoris",
|
||||
"displayName": "Giocatore",
|
||||
"description": "Anagrafica rosa CRAP Volley. Le statistiche (presenze, MVP, ecc.) restano calcolate lato app, non vivono qui."
|
||||
},
|
||||
"options": {
|
||||
"draftAndPublish": false
|
||||
},
|
||||
"pluginOptions": {},
|
||||
"attributes": {
|
||||
"codice": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"unique": true,
|
||||
"regex": "^g[0-9]+$"
|
||||
},
|
||||
"nome": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"numero": {
|
||||
"type": "integer",
|
||||
"required": true
|
||||
},
|
||||
"ruolo": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"nascita": {
|
||||
"type": "date",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreController } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreController('api::giocatore.giocatore');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreRouter } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreRouter('api::giocatore.giocatore');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreService } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreService('api::giocatore.giocatore');
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "match_storici",
|
||||
"info": {
|
||||
"singularName": "match-storico",
|
||||
"pluralName": "match-storicos",
|
||||
"displayName": "Match storico",
|
||||
"description": "Storico partite di campionato CSI."
|
||||
},
|
||||
"options": {
|
||||
"draftAndPublish": false
|
||||
},
|
||||
"pluginOptions": {},
|
||||
"attributes": {
|
||||
"data": {
|
||||
"type": "date",
|
||||
"required": true
|
||||
},
|
||||
"avversario": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"casa": {
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"default": true
|
||||
},
|
||||
"set_nostri": {
|
||||
"type": "integer",
|
||||
"required": true
|
||||
},
|
||||
"set_loro": {
|
||||
"type": "integer",
|
||||
"required": true
|
||||
},
|
||||
"parziali": {
|
||||
"type": "json"
|
||||
},
|
||||
"mvp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreController } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreController('api::match-storico.match-storico');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreRouter } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreRouter('api::match-storico.match-storico');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreService } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreService('api::match-storico.match-storico');
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "righe_classifica",
|
||||
"info": {
|
||||
"singularName": "riga-classifica",
|
||||
"pluralName": "riga-classificas",
|
||||
"displayName": "Riga classifica",
|
||||
"description": "Classifica campionato CSI, una riga per squadra."
|
||||
},
|
||||
"options": {
|
||||
"draftAndPublish": false
|
||||
},
|
||||
"pluginOptions": {},
|
||||
"attributes": {
|
||||
"pos": {
|
||||
"type": "integer",
|
||||
"required": true
|
||||
},
|
||||
"squadra": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"giocate": {
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"default": 0
|
||||
},
|
||||
"vinte": {
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"default": 0
|
||||
},
|
||||
"perse": {
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"default": 0
|
||||
},
|
||||
"set_fatti": {
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"default": 0
|
||||
},
|
||||
"set_subiti": {
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"default": 0
|
||||
},
|
||||
"punti": {
|
||||
"type": "integer",
|
||||
"required": true,
|
||||
"default": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreController } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreController('api::riga-classifica.riga-classifica');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreRouter } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreRouter('api::riga-classifica.riga-classifica');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreService } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreService('api::riga-classifica.riga-classifica');
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"kind": "collectionType",
|
||||
"collectionName": "scout_match_finales",
|
||||
"info": {
|
||||
"singularName": "scout-match-finale",
|
||||
"pluralName": "scout-match-finales",
|
||||
"displayName": "Scout match finale",
|
||||
"description": "Risultato finale di una partita scoutata dal vivo (dato storico, di sola consultazione). L'azione-per-azione resta in scout_live/localStorage durante la partita."
|
||||
},
|
||||
"options": {
|
||||
"draftAndPublish": false
|
||||
},
|
||||
"pluginOptions": {},
|
||||
"attributes": {
|
||||
"match_id": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"unique": true
|
||||
},
|
||||
"data": {
|
||||
"type": "date",
|
||||
"required": true
|
||||
},
|
||||
"avversario": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"casa": {
|
||||
"type": "boolean",
|
||||
"required": true,
|
||||
"default": true
|
||||
},
|
||||
"set_nostri": {
|
||||
"type": "integer",
|
||||
"required": true
|
||||
},
|
||||
"set_loro": {
|
||||
"type": "integer",
|
||||
"required": true
|
||||
},
|
||||
"parziali": {
|
||||
"type": "json"
|
||||
},
|
||||
"mvp": {
|
||||
"type": "string"
|
||||
},
|
||||
"azioni": {
|
||||
"type": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreController } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreController('api::scout-match-finale.scout-match-finale');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreRouter } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreRouter('api::scout-match-finale.scout-match-finale');
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const { createCoreService } = require('@strapi/strapi').factories;
|
||||
|
||||
module.exports = createCoreService('api::scout-match-finale.scout-match-finale');
|
||||
@@ -0,0 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
register(/*{ strapi }*/) {},
|
||||
bootstrap(/*{ strapi }*/) {},
|
||||
};
|
||||
@@ -0,0 +1,389 @@
|
||||
############
|
||||
# Docker compose override files to layer on top of docker-compose.yml.
|
||||
# Native docker compose COMPOSE_FILE: colon-separated list, base file first.
|
||||
# Manage with: ./run.sh config add|remove <name>
|
||||
#
|
||||
# Examples:
|
||||
# COMPOSE_FILE=docker-compose.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
|
||||
#
|
||||
############
|
||||
COMPOSE_FILE=docker-compose.yml
|
||||
|
||||
|
||||
############
|
||||
# Secrets
|
||||
#
|
||||
# YOU MUST CHANGE ALL THE DEFAULT VALUES BELOW BEFORE STARTING
|
||||
# THE CONTAINERS FOR THE FIRST TIME!
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/docker#configuring-and-securing-supabase
|
||||
#
|
||||
# To generate secrets and API keys:
|
||||
# 1. sh utils/generate-keys.sh
|
||||
# 2. sh utils/add-new-auth-keys.sh
|
||||
#
|
||||
############
|
||||
|
||||
# Postgres
|
||||
POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||
|
||||
# Asymmetric key pair (ES256) and opaque API keys
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys
|
||||
#
|
||||
# To generate:
|
||||
# sh ./utils/add-new-auth-keys.sh
|
||||
#
|
||||
# Opaque API key for client-side use (anon role).
|
||||
SUPABASE_PUBLISHABLE_KEY=
|
||||
# Opaque API key for server-side use (service_role). Never expose in client code.
|
||||
SUPABASE_SECRET_KEY=
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric).
|
||||
# Used by Auth.
|
||||
JWT_KEYS=
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# Used by PostgREST, Realtime, Storage to verify tokens.
|
||||
JWT_JWKS=
|
||||
|
||||
# Access to Dashboard
|
||||
DASHBOARD_USERNAME=supabase
|
||||
DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated
|
||||
|
||||
# Encryption key for securing Realtime and Supavisor communications.
|
||||
# (Must be at least 64 characters; generate with: openssl rand -base64 48)
|
||||
SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
|
||||
|
||||
# Encryption key used by Realtime for sensitive fields in the `_realtime` schema.
|
||||
# (Must be exactly 16 characters; generate with: `openssl rand -hex 8`)
|
||||
REALTIME_DB_ENC_KEY=supabaserealtime
|
||||
|
||||
# Encryption key used by Supavisor for storing encrypted configuration.
|
||||
# (Must be exactly 32 characters; generate with: openssl rand -hex 16)
|
||||
VAULT_ENC_KEY=your-32-character-encryption-key
|
||||
|
||||
# Encryption key for securing connection strings used by Studio against postgres-meta.
|
||||
# (Must be at least 32 characters; generate with openssl rand -base64 24)
|
||||
PG_META_CRYPTO_KEY=your-encryption-key-32-chars-min
|
||||
|
||||
# API token for log ingestion used by Logflare and Vector.
|
||||
# (Must be at least 32 characters; generate with openssl rand -base64 24)
|
||||
LOGFLARE_PUBLIC_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-public
|
||||
# API token used for Logflare management operations. Never expose client-side.
|
||||
# (Must be at least 32 characters; generate with openssl rand -base64 24)
|
||||
LOGFLARE_PRIVATE_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-private
|
||||
|
||||
# Access key ID (username-like) for accessing the S3 protocol endpoint in Storage.
|
||||
# (Generate with: openssl rand -hex 16)
|
||||
S3_PROTOCOL_ACCESS_KEY_ID=625729a08b95bf1b7ff351a663f3a23c
|
||||
# Secret key (password-like) used with S3_PROTOCOL_ACCESS_KEY_ID.
|
||||
# (Generate with: openssl rand -hex 32)
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
|
||||
|
||||
|
||||
############
|
||||
# URLs - Configure hostnames below to reflect your actual domain name
|
||||
############
|
||||
|
||||
# Access to Dashboard and REST API
|
||||
SUPABASE_PUBLIC_URL=http://localhost:8000
|
||||
|
||||
# Full external URL of the Auth service, used to construct OAuth callbacks,
|
||||
# SAML endpoints, and email links
|
||||
API_EXTERNAL_URL=http://localhost:8000/auth/v1
|
||||
|
||||
# See also the Auth section below for Site URL and Redirect URLs configuration
|
||||
|
||||
|
||||
############
|
||||
# Database - Postgres configuration
|
||||
############
|
||||
|
||||
# Using default user (postgres)
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_DB=postgres
|
||||
|
||||
# Default configuration includes Supavisor exposing POSTGRES_PORT
|
||||
# Postgres uses POSTGRES_PORT inside the container
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/accessing-postgres
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
|
||||
############
|
||||
# Database pooler
|
||||
############
|
||||
|
||||
# Self-hosted Supabase uses Supavisor as the default database pooler.
|
||||
# If you use the PgBouncer docker-compose override, Supavisor is disabled
|
||||
# and the pooler settings below are used to configure PgBouncer instead.
|
||||
#
|
||||
# Supavisor exposes POSTGRES_PORT and POOLER_PROXY_PORT_TRANSACTION,
|
||||
# POSTGRES_PORT is used for session mode pooling
|
||||
# PgBouncer only exposes POOLER_PROXY_PORT_TRANSACTION.
|
||||
#
|
||||
# Port to use for transaction mode pooling connections
|
||||
POOLER_PROXY_PORT_TRANSACTION=6543
|
||||
|
||||
# Maximum number of PostgreSQL connections Supavisor or PgBouncer opens per pool
|
||||
POOLER_DEFAULT_POOL_SIZE=20
|
||||
|
||||
# Maximum number of client connections Supavisor or PgBouncer accepts per pool
|
||||
POOLER_MAX_CLIENT_CONN=100
|
||||
|
||||
# Unique Supavisor tenant identifier
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/accessing-postgres
|
||||
POOLER_TENANT_ID=your-tenant-id
|
||||
|
||||
# Pool size for internal metadata storage used by Supavisor
|
||||
# This is separate from client connections and used only by Supavisor itself
|
||||
POOLER_DB_POOL_SIZE=5
|
||||
|
||||
|
||||
############
|
||||
# Studio - Configuration for the Dashboard
|
||||
############
|
||||
|
||||
STUDIO_DEFAULT_ORGANIZATION=Default Organization
|
||||
STUDIO_DEFAULT_PROJECT=Default Project
|
||||
|
||||
# Add your OpenAI API key to enable AI Assistant
|
||||
OPENAI_API_KEY=sk-proj-xxxxxxxx
|
||||
|
||||
|
||||
############
|
||||
# Auth - Configuration for the authentication server
|
||||
############
|
||||
|
||||
## General settings
|
||||
|
||||
# Equivalent to "Site URL" and "Redirect URLs" platform configuration options
|
||||
# Documentation: https://supabase.com/docs/guides/auth/redirect-urls
|
||||
SITE_URL=http://localhost:3000
|
||||
ADDITIONAL_REDIRECT_URLS=
|
||||
|
||||
JWT_EXPIRY=3600
|
||||
DISABLE_SIGNUP=false
|
||||
|
||||
## Mailer Config
|
||||
MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"
|
||||
MAILER_URLPATHS_INVITE="/auth/v1/verify"
|
||||
MAILER_URLPATHS_RECOVERY="/auth/v1/verify"
|
||||
MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify"
|
||||
|
||||
## Email auth
|
||||
ENABLE_EMAIL_SIGNUP=true
|
||||
ENABLE_EMAIL_AUTOCONFIRM=false
|
||||
SMTP_ADMIN_EMAIL=admin@example.com
|
||||
SMTP_HOST=supabase-mail
|
||||
SMTP_PORT=2500
|
||||
SMTP_USER=fake_mail_user
|
||||
SMTP_PASS=fake_mail_password
|
||||
SMTP_SENDER_NAME=fake_sender
|
||||
ENABLE_ANONYMOUS_USERS=false
|
||||
|
||||
## Phone auth
|
||||
ENABLE_PHONE_SIGNUP=true
|
||||
ENABLE_PHONE_AUTOCONFIRM=true
|
||||
|
||||
## OAuth / Social login providers
|
||||
|
||||
# Uncomment and fill in the providers you want to enable.
|
||||
# You must ALSO uncomment the matching GOTRUE_EXTERNAL_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-oauth
|
||||
# GOOGLE_ENABLED=false
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_SECRET=
|
||||
|
||||
# GITHUB_ENABLED=false
|
||||
# GITHUB_CLIENT_ID=
|
||||
# GITHUB_SECRET=
|
||||
|
||||
# AZURE_ENABLED=false
|
||||
# AZURE_CLIENT_ID=
|
||||
# AZURE_SECRET=
|
||||
|
||||
# Phone / SMS provider configuration
|
||||
# Uncomment to configure SMS delivery for phone auth and phone MFA.
|
||||
# You must ALSO uncomment the matching GOTRUE_SMS_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-phone-mfa
|
||||
# SMS_PROVIDER=twilio
|
||||
# SMS_OTP_EXP=60
|
||||
# SMS_OTP_LENGTH=6
|
||||
# SMS_MAX_FREQUENCY=60s
|
||||
# SMS_TEMPLATE=Your code is {{ .Code }}
|
||||
|
||||
# SMS_TWILIO_ACCOUNT_SID=
|
||||
# SMS_TWILIO_AUTH_TOKEN=
|
||||
# SMS_TWILIO_MESSAGE_SERVICE_SID=
|
||||
|
||||
# Test OTP: map phone numbers to fixed OTP codes for development
|
||||
# Format: phone1:code1,phone2:code2
|
||||
# SMS_TEST_OTP=
|
||||
|
||||
# Multi-factor authentication (MFA)
|
||||
# Uncomment to change MFA defaults.
|
||||
# You must ALSO uncomment the matching GOTRUE_MFA_* lines in docker-compose.yml
|
||||
|
||||
# App Authenticator (TOTP) - enabled by default
|
||||
# MFA_TOTP_ENROLL_ENABLED=true
|
||||
# MFA_TOTP_VERIFY_ENABLED=true
|
||||
|
||||
# Phone MFA - disabled by default (opt-in)
|
||||
# MFA_PHONE_ENROLL_ENABLED=false
|
||||
# MFA_PHONE_VERIFY_ENABLED=false
|
||||
|
||||
# Maximum MFA factors a user can enroll
|
||||
# MFA_MAX_ENROLLED_FACTORS=10
|
||||
|
||||
## SAML SSO
|
||||
|
||||
# You must ALSO uncomment the matching GOTRUE_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-saml-sso
|
||||
|
||||
# SAML_ENABLED=true
|
||||
# SAML_PRIVATE_KEY=<your-base64-encoded-private-key>
|
||||
|
||||
# Optional: accept encrypted SAML assertions from IdPs (default: false)
|
||||
# SAML_ALLOW_ENCRYPTED_ASSERTIONS=false
|
||||
|
||||
# Optional: how long relay state tokens remain valid (default: 2m0s)
|
||||
# SAML_RELAY_STATE_VALIDITY_PERIOD=2m0s
|
||||
|
||||
# Optional: override the SAML entity ID / ACS base URL
|
||||
# Defaults to API_EXTERNAL_URL if not set
|
||||
# SAML_EXTERNAL_URL=https://supabase.example.com:8000/auth/v1
|
||||
|
||||
# Optional: rate limit on the ACS endpoint (requests per second, default: 15)
|
||||
# SAML_RATE_LIMIT_ASSERTION=15
|
||||
|
||||
|
||||
############
|
||||
# Storage - Configuration for Storage
|
||||
############
|
||||
|
||||
# Check the S3_PROTOCOL_ACCESS_KEY_ID/SECRET above, and
|
||||
# refer to the documentation at:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-s3
|
||||
# to learn how to configure the S3 protocol endpoint
|
||||
|
||||
# S3 bucket when using S3 backend, directory name when using 'file'
|
||||
GLOBAL_S3_BUCKET=stub
|
||||
|
||||
# Used for S3 protocol endpoint configuration
|
||||
REGION=stub
|
||||
|
||||
# Used by MinIO when added via:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||
MINIO_ROOT_USER=supa-storage
|
||||
# Root administrator password for the RustFS or MinIO server.
|
||||
# (Must be 8+ characters; generate with: openssl rand -hex 16)
|
||||
MINIO_ROOT_PASSWORD=secret1234
|
||||
|
||||
# Equivalent to project_ref as described here:
|
||||
# https://supabase.com/docs/guides/storage/s3/authentication#session-token
|
||||
STORAGE_TENANT_ID=stub
|
||||
|
||||
|
||||
############
|
||||
# Functions - Configuration for Edge functions
|
||||
############
|
||||
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-functions
|
||||
|
||||
# NOTE: VERIFY_JWT applies to all functions
|
||||
FUNCTIONS_VERIFY_JWT=false
|
||||
|
||||
|
||||
############
|
||||
# API - Configuration for PostgREST
|
||||
############
|
||||
|
||||
# Postgres schemas exposed via the REST API
|
||||
PGRST_DB_SCHEMAS=public,graphql_public
|
||||
|
||||
# Max number of rows returned by a request
|
||||
PGRST_DB_MAX_ROWS=1000
|
||||
|
||||
# Extra schemas added to the search_path of every request
|
||||
PGRST_DB_EXTRA_SEARCH_PATH=public
|
||||
|
||||
|
||||
############
|
||||
# Logs and Analytics
|
||||
############
|
||||
|
||||
## Vector log collection and routing
|
||||
|
||||
# Docker socket location - required for proper Vector operation
|
||||
DOCKER_SOCKET_LOCATION=/var/run/docker.sock
|
||||
# For Podman use the following:
|
||||
# DOCKER_SOCKET_LOCATION=/run/podman/podman.sock
|
||||
|
||||
## Analytics (Logflare)
|
||||
|
||||
# Check the LOGFLARE_* access token configuration _above_.
|
||||
# If Logflare has to be externally exposed - configure securely!
|
||||
|
||||
# Google Cloud Project details
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/reference/self-hosting-analytics/introduction
|
||||
GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID
|
||||
GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER
|
||||
|
||||
|
||||
############
|
||||
# API gateway
|
||||
############
|
||||
|
||||
# Host port the API gateway (Envoy by default) listens on.
|
||||
API_GW_HTTP_PORT=8000
|
||||
|
||||
# Kong gateway override only (sh run.sh config add kong). KONG_HTTPS_PORT is
|
||||
# Kong's built-in HTTPS listener; KONG_HTTP_PORT is kept as a fallback for
|
||||
# API_GW_HTTP_PORT so existing .env files continue to work.
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
|
||||
# Used internally by the API gateway - DO NOT use in any client or server code.
|
||||
# Pre-signed ES256 JWT "API key" for anon role.
|
||||
ANON_KEY_ASYMMETRIC=
|
||||
# Pre-signed ES256 JWT "API key" for service_role.
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC=
|
||||
|
||||
|
||||
############
|
||||
# imgproxy
|
||||
############
|
||||
|
||||
# Enable webp support
|
||||
IMGPROXY_AUTO_WEBP=true
|
||||
|
||||
|
||||
############
|
||||
# TLS Proxy - Optional Caddy or Nginx reverse proxy with Let's Encrypt
|
||||
############
|
||||
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https
|
||||
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
||||
# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||
|
||||
# Domain name for the proxy (must point to your server)
|
||||
PROXY_DOMAIN=your-domain.example.com
|
||||
|
||||
# Email for Let's Encrypt certificate notifications (nginx only, Caddy uses PROXY_DOMAIN).
|
||||
# This should be a valid email, not a placeholder (otherwise Certbot may fail to start).
|
||||
CERTBOT_EMAIL=admin@example.com
|
||||
@@ -0,0 +1,402 @@
|
||||
# TEMPLATE DI PRODUZIONE — non usare i valori cosi' come sono.
|
||||
#
|
||||
# 1. Copia questo file in .env: cp .env.production.example .env
|
||||
# 2. Rigenera TUTTI i secret (non riusare quelli di sviluppo):
|
||||
# sh utils/generate-keys.sh --update-env
|
||||
# 3. Sostituisci i placeholder tuodominio.it con il dominio reale (un solo hostname: le API
|
||||
# Supabase sono servite sotto /api, instradate per path da Caddy — vedi Caddyfile.example).
|
||||
# Con No-IP gratuito, es. crapp.ddns.net per tutto.
|
||||
# 4. Imposta una DASHBOARD_PASSWORD robusta e, se servono email
|
||||
# (reset password, inviti), un SMTP reale al posto di quello finto.
|
||||
# 5. Avvia con l'override che non espone porte pubblicamente:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
#
|
||||
############
|
||||
# Docker compose override files to layer on top of docker-compose.yml.
|
||||
# Native docker compose COMPOSE_FILE: colon-separated list, base file first.
|
||||
# Manage with: ./run.sh config add|remove <name>
|
||||
#
|
||||
# Examples:
|
||||
# COMPOSE_FILE=docker-compose.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
|
||||
#
|
||||
############
|
||||
COMPOSE_FILE=docker-compose.yml
|
||||
|
||||
|
||||
############
|
||||
# Secrets
|
||||
#
|
||||
# YOU MUST CHANGE ALL THE DEFAULT VALUES BELOW BEFORE STARTING
|
||||
# THE CONTAINERS FOR THE FIRST TIME!
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/docker#configuring-and-securing-supabase
|
||||
#
|
||||
# To generate secrets and API keys:
|
||||
# 1. sh utils/generate-keys.sh
|
||||
# 2. sh utils/add-new-auth-keys.sh
|
||||
#
|
||||
############
|
||||
|
||||
# Postgres
|
||||
POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET=your-super-secret-jwt-token-with-at-least-32-characters-long
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||
|
||||
# Asymmetric key pair (ES256) and opaque API keys
|
||||
#
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys
|
||||
#
|
||||
# To generate:
|
||||
# sh ./utils/add-new-auth-keys.sh
|
||||
#
|
||||
# Opaque API key for client-side use (anon role).
|
||||
SUPABASE_PUBLISHABLE_KEY=
|
||||
# Opaque API key for server-side use (service_role). Never expose in client code.
|
||||
SUPABASE_SECRET_KEY=
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric).
|
||||
# Used by Auth.
|
||||
JWT_KEYS=
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# Used by PostgREST, Realtime, Storage to verify tokens.
|
||||
JWT_JWKS=
|
||||
|
||||
# Access to Dashboard
|
||||
DASHBOARD_USERNAME=supabase
|
||||
DASHBOARD_PASSWORD=CAMBIAMI-password-robusta
|
||||
|
||||
# Encryption key for securing Realtime and Supavisor communications.
|
||||
# (Must be at least 64 characters; generate with: openssl rand -base64 48)
|
||||
SECRET_KEY_BASE=UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq
|
||||
|
||||
# Encryption key used by Realtime for sensitive fields in the `_realtime` schema.
|
||||
# (Must be exactly 16 characters; generate with: `openssl rand -hex 8`)
|
||||
REALTIME_DB_ENC_KEY=supabaserealtime
|
||||
|
||||
# Encryption key used by Supavisor for storing encrypted configuration.
|
||||
# (Must be exactly 32 characters; generate with: openssl rand -hex 16)
|
||||
VAULT_ENC_KEY=your-32-character-encryption-key
|
||||
|
||||
# Encryption key for securing connection strings used by Studio against postgres-meta.
|
||||
# (Must be at least 32 characters; generate with openssl rand -base64 24)
|
||||
PG_META_CRYPTO_KEY=your-encryption-key-32-chars-min
|
||||
|
||||
# API token for log ingestion used by Logflare and Vector.
|
||||
# (Must be at least 32 characters; generate with openssl rand -base64 24)
|
||||
LOGFLARE_PUBLIC_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-public
|
||||
# API token used for Logflare management operations. Never expose client-side.
|
||||
# (Must be at least 32 characters; generate with openssl rand -base64 24)
|
||||
LOGFLARE_PRIVATE_ACCESS_TOKEN=your-super-secret-and-long-logflare-key-private
|
||||
|
||||
# Access key ID (username-like) for accessing the S3 protocol endpoint in Storage.
|
||||
# (Generate with: openssl rand -hex 16)
|
||||
S3_PROTOCOL_ACCESS_KEY_ID=625729a08b95bf1b7ff351a663f3a23c
|
||||
# Secret key (password-like) used with S3_PROTOCOL_ACCESS_KEY_ID.
|
||||
# (Generate with: openssl rand -hex 32)
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET=850181e4652dd023b7a98c58ae0d2d34bd487ee0cc3254aed6eda37307425907
|
||||
|
||||
|
||||
############
|
||||
# URLs - Configure hostnames below to reflect your actual domain name
|
||||
############
|
||||
|
||||
# Access to Dashboard and REST API (dietro Caddy: /api/* -> questo gateway, path stripped)
|
||||
SUPABASE_PUBLIC_URL=https://tuodominio.it/api
|
||||
|
||||
# Full external URL of the Auth service, used to construct OAuth callbacks,
|
||||
# SAML endpoints, and email links
|
||||
API_EXTERNAL_URL=https://tuodominio.it/api/auth/v1
|
||||
|
||||
# See also the Auth section below for Site URL and Redirect URLs configuration
|
||||
|
||||
|
||||
############
|
||||
# Database - Postgres configuration
|
||||
############
|
||||
|
||||
# Using default user (postgres)
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_DB=postgres
|
||||
|
||||
# Default configuration includes Supavisor exposing POSTGRES_PORT
|
||||
# Postgres uses POSTGRES_PORT inside the container
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/accessing-postgres
|
||||
POSTGRES_PORT=5432 # non esporre su internet, vedi docker-compose.prod.yml
|
||||
|
||||
|
||||
############
|
||||
# Database pooler
|
||||
############
|
||||
|
||||
# Self-hosted Supabase uses Supavisor as the default database pooler.
|
||||
# If you use the PgBouncer docker-compose override, Supavisor is disabled
|
||||
# and the pooler settings below are used to configure PgBouncer instead.
|
||||
#
|
||||
# Supavisor exposes POSTGRES_PORT and POOLER_PROXY_PORT_TRANSACTION,
|
||||
# POSTGRES_PORT is used for session mode pooling
|
||||
# PgBouncer only exposes POOLER_PROXY_PORT_TRANSACTION.
|
||||
#
|
||||
# Port to use for transaction mode pooling connections
|
||||
POOLER_PROXY_PORT_TRANSACTION=6543
|
||||
|
||||
# Maximum number of PostgreSQL connections Supavisor or PgBouncer opens per pool
|
||||
POOLER_DEFAULT_POOL_SIZE=20
|
||||
|
||||
# Maximum number of client connections Supavisor or PgBouncer accepts per pool
|
||||
POOLER_MAX_CLIENT_CONN=100
|
||||
|
||||
# Unique Supavisor tenant identifier
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/accessing-postgres
|
||||
POOLER_TENANT_ID=your-tenant-id
|
||||
|
||||
# Pool size for internal metadata storage used by Supavisor
|
||||
# This is separate from client connections and used only by Supavisor itself
|
||||
POOLER_DB_POOL_SIZE=5
|
||||
|
||||
|
||||
############
|
||||
# Studio - Configuration for the Dashboard
|
||||
############
|
||||
|
||||
STUDIO_DEFAULT_ORGANIZATION=Default Organization
|
||||
STUDIO_DEFAULT_PROJECT=Default Project
|
||||
|
||||
# Add your OpenAI API key to enable AI Assistant
|
||||
OPENAI_API_KEY=sk-proj-xxxxxxxx
|
||||
|
||||
|
||||
############
|
||||
# Auth - Configuration for the authentication server
|
||||
############
|
||||
|
||||
## General settings
|
||||
|
||||
# Equivalent to "Site URL" and "Redirect URLs" platform configuration options
|
||||
# Documentation: https://supabase.com/docs/guides/auth/redirect-urls
|
||||
SITE_URL=https://tuodominio.it
|
||||
ADDITIONAL_REDIRECT_URLS=
|
||||
|
||||
JWT_EXPIRY=3600
|
||||
DISABLE_SIGNUP=false
|
||||
|
||||
## Mailer Config
|
||||
MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"
|
||||
MAILER_URLPATHS_INVITE="/auth/v1/verify"
|
||||
MAILER_URLPATHS_RECOVERY="/auth/v1/verify"
|
||||
MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify"
|
||||
|
||||
## Email auth
|
||||
ENABLE_EMAIL_SIGNUP=true
|
||||
ENABLE_EMAIL_AUTOCONFIRM=false
|
||||
SMTP_ADMIN_EMAIL=admin@example.com
|
||||
SMTP_HOST=supabase-mail
|
||||
SMTP_PORT=2500
|
||||
SMTP_USER=fake_mail_user
|
||||
SMTP_PASS=fake_mail_password
|
||||
SMTP_SENDER_NAME=fake_sender
|
||||
ENABLE_ANONYMOUS_USERS=false
|
||||
|
||||
## Phone auth
|
||||
ENABLE_PHONE_SIGNUP=true
|
||||
ENABLE_PHONE_AUTOCONFIRM=true
|
||||
|
||||
## OAuth / Social login providers
|
||||
|
||||
# Uncomment and fill in the providers you want to enable.
|
||||
# You must ALSO uncomment the matching GOTRUE_EXTERNAL_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-oauth
|
||||
# GOOGLE_ENABLED=false
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_SECRET=
|
||||
|
||||
# GITHUB_ENABLED=false
|
||||
# GITHUB_CLIENT_ID=
|
||||
# GITHUB_SECRET=
|
||||
|
||||
# AZURE_ENABLED=false
|
||||
# AZURE_CLIENT_ID=
|
||||
# AZURE_SECRET=
|
||||
|
||||
# Phone / SMS provider configuration
|
||||
# Uncomment to configure SMS delivery for phone auth and phone MFA.
|
||||
# You must ALSO uncomment the matching GOTRUE_SMS_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-phone-mfa
|
||||
# SMS_PROVIDER=twilio
|
||||
# SMS_OTP_EXP=60
|
||||
# SMS_OTP_LENGTH=6
|
||||
# SMS_MAX_FREQUENCY=60s
|
||||
# SMS_TEMPLATE=Your code is {{ .Code }}
|
||||
|
||||
# SMS_TWILIO_ACCOUNT_SID=
|
||||
# SMS_TWILIO_AUTH_TOKEN=
|
||||
# SMS_TWILIO_MESSAGE_SERVICE_SID=
|
||||
|
||||
# Test OTP: map phone numbers to fixed OTP codes for development
|
||||
# Format: phone1:code1,phone2:code2
|
||||
# SMS_TEST_OTP=
|
||||
|
||||
# Multi-factor authentication (MFA)
|
||||
# Uncomment to change MFA defaults.
|
||||
# You must ALSO uncomment the matching GOTRUE_MFA_* lines in docker-compose.yml
|
||||
|
||||
# App Authenticator (TOTP) - enabled by default
|
||||
# MFA_TOTP_ENROLL_ENABLED=true
|
||||
# MFA_TOTP_VERIFY_ENABLED=true
|
||||
|
||||
# Phone MFA - disabled by default (opt-in)
|
||||
# MFA_PHONE_ENROLL_ENABLED=false
|
||||
# MFA_PHONE_VERIFY_ENABLED=false
|
||||
|
||||
# Maximum MFA factors a user can enroll
|
||||
# MFA_MAX_ENROLLED_FACTORS=10
|
||||
|
||||
## SAML SSO
|
||||
|
||||
# You must ALSO uncomment the matching GOTRUE_* lines in docker-compose.yml
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/self-hosted-saml-sso
|
||||
|
||||
# SAML_ENABLED=true
|
||||
# SAML_PRIVATE_KEY=<your-base64-encoded-private-key>
|
||||
|
||||
# Optional: accept encrypted SAML assertions from IdPs (default: false)
|
||||
# SAML_ALLOW_ENCRYPTED_ASSERTIONS=false
|
||||
|
||||
# Optional: how long relay state tokens remain valid (default: 2m0s)
|
||||
# SAML_RELAY_STATE_VALIDITY_PERIOD=2m0s
|
||||
|
||||
# Optional: override the SAML entity ID / ACS base URL
|
||||
# Defaults to API_EXTERNAL_URL if not set
|
||||
# SAML_EXTERNAL_URL=https://supabase.example.com:8000/auth/v1
|
||||
|
||||
# Optional: rate limit on the ACS endpoint (requests per second, default: 15)
|
||||
# SAML_RATE_LIMIT_ASSERTION=15
|
||||
|
||||
|
||||
############
|
||||
# Storage - Configuration for Storage
|
||||
############
|
||||
|
||||
# Check the S3_PROTOCOL_ACCESS_KEY_ID/SECRET above, and
|
||||
# refer to the documentation at:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-s3
|
||||
# to learn how to configure the S3 protocol endpoint
|
||||
|
||||
# S3 bucket when using S3 backend, directory name when using 'file'
|
||||
GLOBAL_S3_BUCKET=stub
|
||||
|
||||
# Used for S3 protocol endpoint configuration
|
||||
REGION=stub
|
||||
|
||||
# Used by MinIO when added via:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||
MINIO_ROOT_USER=supa-storage
|
||||
# Root administrator password for the RustFS or MinIO server.
|
||||
# (Must be 8+ characters; generate with: openssl rand -hex 16)
|
||||
MINIO_ROOT_PASSWORD=secret1234
|
||||
|
||||
# Equivalent to project_ref as described here:
|
||||
# https://supabase.com/docs/guides/storage/s3/authentication#session-token
|
||||
STORAGE_TENANT_ID=stub
|
||||
|
||||
|
||||
############
|
||||
# Functions - Configuration for Edge functions
|
||||
############
|
||||
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-functions
|
||||
|
||||
# NOTE: VERIFY_JWT applies to all functions
|
||||
FUNCTIONS_VERIFY_JWT=false
|
||||
|
||||
|
||||
############
|
||||
# API - Configuration for PostgREST
|
||||
############
|
||||
|
||||
# Postgres schemas exposed via the REST API
|
||||
PGRST_DB_SCHEMAS=public,graphql_public
|
||||
|
||||
# Max number of rows returned by a request
|
||||
PGRST_DB_MAX_ROWS=1000
|
||||
|
||||
# Extra schemas added to the search_path of every request
|
||||
PGRST_DB_EXTRA_SEARCH_PATH=public
|
||||
|
||||
|
||||
############
|
||||
# Logs and Analytics
|
||||
############
|
||||
|
||||
## Vector log collection and routing
|
||||
|
||||
# Docker socket location - required for proper Vector operation
|
||||
DOCKER_SOCKET_LOCATION=/var/run/docker.sock
|
||||
# For Podman use the following:
|
||||
# DOCKER_SOCKET_LOCATION=/run/podman/podman.sock
|
||||
|
||||
## Analytics (Logflare)
|
||||
|
||||
# Check the LOGFLARE_* access token configuration _above_.
|
||||
# If Logflare has to be externally exposed - configure securely!
|
||||
|
||||
# Google Cloud Project details
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/reference/self-hosting-analytics/introduction
|
||||
GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID
|
||||
GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER
|
||||
|
||||
|
||||
############
|
||||
# API gateway
|
||||
############
|
||||
|
||||
# Host port the API gateway (Envoy by default) listens on.
|
||||
API_GW_HTTP_PORT=8000
|
||||
|
||||
# Kong gateway override only (sh run.sh config add kong). KONG_HTTPS_PORT is
|
||||
# Kong's built-in HTTPS listener; KONG_HTTP_PORT is kept as a fallback for
|
||||
# API_GW_HTTP_PORT so existing .env files continue to work.
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
|
||||
# Used internally by the API gateway - DO NOT use in any client or server code.
|
||||
# Pre-signed ES256 JWT "API key" for anon role.
|
||||
ANON_KEY_ASYMMETRIC=
|
||||
# Pre-signed ES256 JWT "API key" for service_role.
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC=
|
||||
|
||||
|
||||
############
|
||||
# imgproxy
|
||||
############
|
||||
|
||||
# Enable webp support
|
||||
IMGPROXY_AUTO_WEBP=true
|
||||
|
||||
|
||||
############
|
||||
# TLS Proxy - Optional Caddy or Nginx reverse proxy with Let's Encrypt
|
||||
############
|
||||
|
||||
# Documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https
|
||||
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
|
||||
# docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d
|
||||
|
||||
# Domain name for the proxy (must point to your server)
|
||||
PROXY_DOMAIN=your-domain.example.com
|
||||
|
||||
# Email for Let's Encrypt certificate notifications (nginx only, Caddy uses PROXY_DOMAIN).
|
||||
# This should be a valid email, not a placeholder (otherwise Certbot may fail to start).
|
||||
CERTBOT_EMAIL=admin@example.com
|
||||
@@ -0,0 +1,17 @@
|
||||
* text=auto
|
||||
|
||||
*.md eol=lf
|
||||
|
||||
*.env eol=lf
|
||||
.env.example eol=lf
|
||||
|
||||
*.sh eol=lf
|
||||
*.sql eol=lf
|
||||
*.yml eol=lf
|
||||
*.yaml eol=lf
|
||||
*.ts eol=lf
|
||||
*.exs eol=lf
|
||||
*.conf eol=lf
|
||||
*.tpl eol=lf
|
||||
Caddyfile eol=lf
|
||||
Dockerfile* eol=lf
|
||||
@@ -0,0 +1,16 @@
|
||||
volumes/db/data
|
||||
volumes/storage
|
||||
volumes/snippets
|
||||
volumes/functions/**
|
||||
!volumes/functions/deno.json*
|
||||
!volumes/functions/main/
|
||||
volumes/functions/main/**
|
||||
!volumes/functions/main/index.ts
|
||||
!volumes/functions/hello/
|
||||
volumes/functions/hello/**
|
||||
!volumes/functions/hello/index.ts
|
||||
.env
|
||||
test.http
|
||||
docker-compose.override.yml
|
||||
.supabase-version
|
||||
backups
|
||||
@@ -0,0 +1,676 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Supabase self-hosted Docker configuration.
|
||||
|
||||
Changes are grouped by service rather than by change type. See [versions.md](./versions.md) for complete image version history and rollback information.
|
||||
|
||||
See per-service updates below for details. Only the most important changes relevant to [self-hosted Supabase](https://supabase.com/docs/guides/self-hosting) are included here. For the full list of changes, refer to the release notes and changelogs of each individual service.
|
||||
|
||||
**Note:** Configuration updates marked with "requires [...] update" are already included in the latest version of the repository. Pull the latest changes or refer to the linked PR for manual updates. After updating `docker-compose.yml`, pull the latest images and recreate containers - use `docker compose pull && docker compose down && docker compose up -d`.
|
||||
|
||||
---
|
||||
|
||||
## [0.8.0](https://github.com/supabase/supabase/releases/tag/self-hosted/v0.8.0) - 2026-08-11
|
||||
|
||||
⚠️ **Note:** This update contains **breaking changes**. Make sure to read the **important** details below:
|
||||
- Envoy is now the default API gateway, replacing Kong. Kong stays available as an opt-in override with `sh run.sh config add kong`. See the [heads-up discussion](https://github.com/orgs/supabase/discussions/48048) and [Update Your Self-Hosted Deployment](https://supabase.com/docs/guides/self-hosting/updating) - PR [#48153](https://github.com/supabase/supabase/pull/48153)
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Added `API_GW_HTTP_PORT` (falls back to `KONG_HTTP_PORT`; requires `.env` and `docker-compose.yml` update) - PR [#48153](https://github.com/supabase/supabase/pull/48153)
|
||||
|
||||
### Documentation
|
||||
- Updated several self-hosting how-to guides to reflect the current configuration - PR [#48153](https://github.com/supabase/supabase/pull/48153)
|
||||
- Updated architecture diagram for self-hosted Supabase - PR [#48763](https://github.com/supabase/supabase/pull/48763)
|
||||
|
||||
### Utils and tests
|
||||
- Updated `tests/` to match the API gateway switch to Envoy - PR [#48153](https://github.com/supabase/supabase/pull/48153)
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Changed the default API gateway from Kong to Envoy; the `kong` service is now `api-gw` (requires `docker-compose.yml` update) - PR [#48153](https://github.com/supabase/supabase/pull/48153)
|
||||
- Changed `docker-compose.envoy.yml` to a no-op shim now that Envoy is the default (requires `docker-compose.envoy.yml` update) - PR [#48153](https://github.com/supabase/supabase/pull/48153)
|
||||
- Added an optional override for Kong (requires new `docker-compose.kong.yml`) - PR [#48153](https://github.com/supabase/supabase/pull/48153)
|
||||
- Updated the Caddy and nginx reverse proxies to forward to `api-gw` (requires `docker-compose.caddy.yml`, `docker-compose.nginx.yml`, `volumes/proxy/caddy/Caddyfile`, and `volumes/proxy/nginx/supabase-nginx.conf.tpl` update) - PR [#48153](https://github.com/supabase/supabase/pull/48153)
|
||||
|
||||
---
|
||||
|
||||
## [0.7.2](https://github.com/supabase/supabase/releases/tag/self-hosted/v0.7.2) - 2026-08-04
|
||||
|
||||
### Utils and tests
|
||||
- Fixed `update.sh` overwriting itself during an update; the new `update.sh` is now staged as `update.sh.new` for review instead of replacing the running script - PR [#48690](https://github.com/supabase/supabase/pull/48690)
|
||||
- `update.sh` now fetches only the `docker/` directory (partial clone), making updates substantially faster and lighter - PR [#48690](https://github.com/supabase/supabase/pull/48690)
|
||||
|
||||
---
|
||||
|
||||
## [0.7.1](https://github.com/supabase/supabase/releases/tag/self-hosted/v0.7.1) - 2026-08-03
|
||||
|
||||
### Configuration
|
||||
- Added `SUPABASE_JWKS` configuration for Edge Functions to `docker-compose.yml` - PR [#45635](https://github.com/supabase/supabase/pull/45635)
|
||||
- Added `upgrades.json` - a version-keyed manifest to gate breaking changes (required for `update.sh`) - PR [#47851](https://github.com/supabase/supabase/pull/47851)
|
||||
- Updated `.gitignore` (required for `update.sh`)
|
||||
|
||||
### Documentation
|
||||
- Added new how-to guides ([Custom Postgres Extensions](https://supabase.com/docs/guides/self-hosting/custom-postgres-extensions) and [Update Your Self-Hosted Deployment](https://supabase.com/docs/guides/self-hosting/updating)) - PR [#48203](https://github.com/supabase/supabase/pull/48203), PR [#48535](https://github.com/supabase/supabase/pull/48535)
|
||||
|
||||
### Utils and tests
|
||||
- Added base version stamp to `setup.sh` (saved in `.supabase-version`, required for `update.sh`) - PR [#47848](https://github.com/supabase/supabase/pull/47848)
|
||||
- Added `update.sh`. Refer to [Update Your Self-Hosted Deployment](https://supabase.com/docs/guides/self-hosting/updating) - PR [#47851](https://github.com/supabase/supabase/pull/47851)
|
||||
- Added `SUPABASE_JWKS` configuration for Edge Functions to `utils/add-new-auth-keys.sh` - PR [#45635](https://github.com/supabase/supabase/pull/45635)
|
||||
- Updated `tests/test-s3.sh` and `test-s3-backend.sh` - PR [#48500](https://github.com/supabase/supabase/pull/48500)
|
||||
|
||||
### API gateway
|
||||
- Updated Kong to `3.9.3`
|
||||
- Added `KONG_DNS_VALID_TTL` configuration environment variable (requires `docker-compose.yml` update) - PR [#47846](https://github.com/supabase/supabase/pull/47846)
|
||||
- Updated Envoy to `1.39.0` (requires `docker-compose.envoy.yml` update)
|
||||
- Updated [nginx-certbot](https://github.com/JonasAlfredsson/docker-nginx-certbot) to `6.2.0-nginx1.31.3` (requires `docker-compose.nginx.yml` update)
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.08.03-sha-022b374`
|
||||
- Fixed URL generation for Edge Functions - PR [#47861](https://github.com/supabase/supabase/pull/47861) (via [@7ttp](https://github.com/7ttp))
|
||||
- Fixed the Logs tab visibility in **Auth > Users** - PR [#48122](https://github.com/supabase/supabase/pull/48122) (via [@luizfelmach](https://github.com/luizfelmach/))
|
||||
|
||||
### Storage
|
||||
- Changed RustFS image to `1.0.0-beta.11` temporarily (requires `docker-compose.rustfs.yml` update) - PR [#48500](https://github.com/supabase/supabase/pull/48500)
|
||||
|
||||
### Edge Runtime
|
||||
- Changed JWKS configuration mechanism for main worker (requires `docker-compose.yml` and `volumes/functions/main/index.ts` update) - PR [#45635](https://github.com/supabase/supabase/pull/45635)
|
||||
|
||||
---
|
||||
|
||||
## [0.7.0](https://github.com/supabase/supabase/releases/tag/self-hosted/v0.7.0) - 2026-07-07
|
||||
|
||||
⚠️ **Note:** This update contains **breaking changes**:
|
||||
- Access to the OpenAPI spec at `/rest/v1/` via the anon (publishable) key has been removed. Requests using the service role or new secret API key are unaffected, and data access via `/rest/v1/your_table` or any client library continues to work as-is. See discussion [#42949](https://github.com/orgs/supabase/discussions/42949)
|
||||
- `API_EXTERNAL_URL` has been updated to include the `/auth/v1` path prefix (e.g. `http://localhost:8000/auth/v1`), aligning self-hosted with the platform and CLI. This makes custom OAuth providers work out of the box and moves SAML SSO endpoints to `/auth/v1/sso/saml/*`. See discussion [#47093](https://github.com/orgs/supabase/discussions/47093) and PR [#47640](https://github.com/supabase/supabase/pull/47640)
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Added `KONG_ROUTER_FLAVOR` to the compose configuration for Kong (requires `docker-compose.yml` update) - PR [#45462](https://github.com/supabase/supabase/pull/45462)
|
||||
- ⚠️ Changed the default `API_EXTERNAL_URL` in `.env.example` to contain `/auth/v1` - PR [#47640](https://github.com/supabase/supabase/pull/47640)
|
||||
- ⚠️ Changed the default `PGRST_DB_SCHEMAS` to `public,graphql_public` in `.env.example` to avoid exposing `storage` (a protected schema)
|
||||
|
||||
### Documentation
|
||||
- Minor updates to the how-to guides following the configuration changes
|
||||
|
||||
### Utils and tests
|
||||
- Updated `setup.sh` to match the new `API_EXTERNAL_URL` configuration
|
||||
- Updated `utils/generate-keys.sh` to also generate a unique `REALTIME_DB_ENC_KEY`
|
||||
- Updated `tests/test-self-hosted.sh` and `tests/test-auth-keys.sh` to reflect the changes in the API gateway configuration
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Updated Kong and Envoy configuration to restrict access to PostgREST `/rest/v1/` (requires `docker-compose.yml`, `volumes/api/kong.yml` and `volumes/api/envoy` update) - PR [#45462](https://github.com/supabase/supabase/pull/45462) (via [@luizfelmach](https://github.com/luizfelmach/))
|
||||
- ⚠️ Updated Kong and Envoy configuration to match the new `/auth/v1/sso` routing for SAML SSO (requires `docker-compose.yml`, `volumes/api/kong.yml` and `volumes/api/envoy` update) - PR [#47640](https://github.com/supabase/supabase/pull/47640)
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.07.07-sha-a6a04f2`
|
||||
- Fixed the local SQL snippets not being shown in the SQL Editor - PR [#47403](https://github.com/supabase/supabase/pull/47403), PR [#47409](https://github.com/supabase/supabase/pull/47409)
|
||||
- Fixed the exposed schemas and tables UI to properly reflect non-platform configuration (**Data API > Settings**) - PR [#47511](https://github.com/supabase/supabase/pull/47511)
|
||||
- Fixed the behavior of the type generator (**Data API > Docs**) - PR [#47577](https://github.com/supabase/supabase/pull/47577)
|
||||
|
||||
### Auth
|
||||
- ⚠️ Changed Auth configuration placeholders to match the new default `API_EXTERNAL_URL` (requires `docker-compose.yml` update) - PR [#47640](https://github.com/supabase/supabase/pull/47640)
|
||||
- ⚠️ Changed `GOTRUE_JWT_ISSUER` to match the new default `API_EXTERNAL_URL` (requires `docker-compose.yml` update) - PR [#47640](https://github.com/supabase/supabase/pull/47640)
|
||||
|
||||
### Realtime
|
||||
- ⚠️ Added a new configuration variable `REALTIME_DB_ENC_KEY` for Realtime with a fallback to the default value (requires `docker-compose.yml` update) - PR [#46021](https://github.com/supabase/supabase/pull/46021)
|
||||
|
||||
---
|
||||
|
||||
## [0.6.0](https://github.com/supabase/supabase/releases/tag/self-hosted/v0.6.0) - 2026-06-17
|
||||
|
||||
⚠️ **Note:** This update contains **breaking changes**. Make sure to read the **important** details below:
|
||||
- **Postgres 17 is now the default**. Do not start Postgres 17 on an existing Postgres 15 data directory. See the [Upgrade to Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) guide. Check the **Configuration** and **Postgres** sections for additional information
|
||||
- API gateway configuration includes a **security fix** for Realtime routes - it is **strongly recommended** to add this update to any self-hosted Supabase instance running Realtime
|
||||
- Studio and Postgres Meta configuration now use `postgres` and not `supabase_admin` to connect to Postgres
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Changed the default Postgres image to `supabase/postgres:17.6.1.136` - PR [#46981](https://github.com/supabase/supabase/pull/46981)
|
||||
- ⚠️ Added `docker-compose.pg15.yml` - for deployments not yet upgraded, and as the rollback target for `utils/upgrade-pg17.sh` - PR [#46981](https://github.com/supabase/supabase/pull/46981)
|
||||
- Updated `docker-compose.pg17.yml` to match the new default - PR [#46981](https://github.com/supabase/supabase/pull/46981)
|
||||
|
||||
### Documentation
|
||||
- Updated the [Upgrade to Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) how-to - PR [#46989](https://github.com/supabase/supabase/pull/46989)
|
||||
- Updated the [New API Keys](https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys) and [Envoy API Gateway](https://supabase.com/docs/guides/self-hosting/self-hosted-envoy) how-to guides - PR [#46856](https://github.com/supabase/supabase/pull/46856)
|
||||
- Updated [CONFIG.md](CONFIG.md) - PR [#47022](https://github.com/supabase/supabase/pull/47022)
|
||||
|
||||
### Utils and tests
|
||||
- Updated `utils/upgrade-pg17.sh` (bumped Postgres image, added additional migrations), and `tests/test-pg17-upgrade.sh` (added tests for pg_cron) - PR [#46981](https://github.com/supabase/supabase/pull/46981)
|
||||
- Updated `tests/test-self-hosted.sh` (added tests for resumable upload, modified tests for Realtime and GraphQL) - PR [#46731](https://github.com/supabase/supabase/pull/46731), PR [#46856](https://github.com/supabase/supabase/pull/46856), PR [#46981](https://github.com/supabase/supabase/pull/46981)
|
||||
- Updated `tests/test-auth-keys.sh` (modified tests for Realtime) - PR [#46856](https://github.com/supabase/supabase/pull/46856)
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Updated Kong and Envoy configuration to block access to Realtime `/api/tenants` and `/api/openapi` endpoints. This is a **security fix** (requires `volumes/api/kong.yml` and `volumes/api/envoy` update) - PR [#46856](https://github.com/supabase/supabase/pull/46856)
|
||||
- Updated entrypoint for Kong to use `/bin/sh` (requires `docker-compose.yml` update) - PR [#46873](https://github.com/supabase/supabase/pull/46873)
|
||||
|
||||
### Studio
|
||||
- ⚠️ Updated `studio` configuration to use `postgres` instead of `supabase_admin` to connect to Postgres (requires `docker-compose.yml` update). See discussion [#46081](https://github.com/orgs/supabase/discussions/46081) and the [how-to guide](https://supabase.com/docs/guides/self-hosting/remove-superuser-access) for important information - PR [#47022](https://github.com/supabase/supabase/pull/47022)
|
||||
|
||||
### PostgREST
|
||||
- Added healthcheck for `rest` (requires `docker-compose.yml` update) - PR [#46658](https://github.com/supabase/supabase/pull/46658)
|
||||
|
||||
### Postgres Meta
|
||||
- ⚠️ Updated `meta` configuration to use `postgres` instead of `supabase_admin` to connect to Postgres (requires `docker-compose.yml` update) - PR [#47022](https://github.com/supabase/supabase/pull/47022)
|
||||
|
||||
### Edge Runtime
|
||||
- Added healthcheck for `functions` (requires `docker-compose.yml` update) - PR [#46655](https://github.com/supabase/supabase/pull/46655)
|
||||
|
||||
### Postgres
|
||||
- ⚠️ Updated the default image to `17.6.1.136` (from `15.8.1.085`). `pg_graphql` is now **disabled by default** on fresh installs. Databases that already use GraphQL keep it after an upgrade. See discussion [#46080](https://github.com/orgs/supabase/discussions/46080) and the [how-to guide](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) for more information - PR [#46981](https://github.com/supabase/supabase/pull/46981)
|
||||
|
||||
---
|
||||
|
||||
## [0.5.0](https://github.com/supabase/supabase/releases/tag/self-hosted/v0.5.0) - 2026-06-03
|
||||
|
||||
⚠️ **Note:** This update includes **important changes**. Please check the details below.
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Logs and analytics are now [optional](https://github.com/orgs/supabase/discussions/46084) and were removed from the default `docker-compose.yml`. A new `docker-compose.logs.yml` override has been added. Check the main [configuration guide](https://supabase.com/docs/guides/self-hosting/docker#enabling-analytics) and the changes to Studio below for more information - PR [#45327](https://github.com/supabase/supabase/pull/45327) (via [@luizfelmach](https://github.com/luizfelmach/))
|
||||
- ⚠️ Added `COMPOSE_FILE` to `.env.example` for configuring compose overrides (also used by `run.sh`) - PR [#45603](https://github.com/supabase/supabase/pull/45603)
|
||||
|
||||
### Documentation
|
||||
- Added a new [reference list](https://github.com/supabase/supabase/blob/master/docker/CONFIG.md) of all configuration environment variables - PR [#46124](https://github.com/supabase/supabase/pull/46124)
|
||||
- Updated the main installation and configuration [guide](https://supabase.com/docs/guides/self-hosting/docker) (added "quick start" path and opt-in for logs and analytics; removed the legacy JWT secrets generator) - PR [#46416](https://github.com/supabase/supabase/pull/46416), PR [#45359](https://github.com/supabase/supabase/pull/45359)
|
||||
- Updated the logs and analytics [how-to guide](https://supabase.com/docs/reference/self-hosting-analytics/introduction) - PR [#46452](https://github.com/supabase/supabase/pull/46452)
|
||||
|
||||
### Utils
|
||||
- Added `setup.sh` and `run.sh` to support quick start and easier management of the compose configuration - PR [#45603](https://github.com/supabase/supabase/pull/45603)
|
||||
- Updated `utils/add-new-auth-keys.sh` and `utils/rotate-new-api-key.sh` to remove the dependency on OpenSSL and Node.js - PR [#45941](https://github.com/supabase/supabase/pull/45941)
|
||||
- Updated `tests/test-container-logs.sh` to skip checks for `kong`, `analytics` and `vector` when the services are not running - PR [#46099](https://github.com/supabase/supabase/pull/46099)
|
||||
|
||||
### API gateway
|
||||
- Updated Envoy version to `1.38.0` (see `docker-compose.envoy.yml`) - PR [#46023](https://github.com/supabase/supabase/pull/46023)
|
||||
- Updated Envoy configuration to address a discrepancy in API key checking (requires `volumes/api/envoy` update) - PR [#46023](https://github.com/supabase/supabase/pull/46023)
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.06.03-sha-0bca601`
|
||||
- ⚠️ Added `ENABLED_FEATURES_LOGS_ALL` to Studio service configuration (requires `docker-compose.yml` update) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
|
||||
- ⚠️ Added `SUPABASE_PUBLISHABLE_KEY` and `SUPABASE_SECRET_KEY` to Studio service configuration (requires `docker-compose.yml` update) - PR [#46173](https://github.com/supabase/supabase/pull/46173)
|
||||
- ⚠️ Added `start_period` to Studio healthcheck for more reliable cold-boot on slower hosts (requires `docker-compose.yml` update) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
|
||||
- Fixed incorrect connection strings in the connect sheet for self-hosted environments - PR [#46217](https://github.com/supabase/supabase/pull/46217)
|
||||
- Updated project home and functions page, and added a minimal project settings implementation - PR [#46544](https://github.com/supabase/supabase/pull/46544), PR [#46550](https://github.com/supabase/supabase/pull/46550), PR [#46554](https://github.com/supabase/supabase/pull/46554)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.189.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.189.0)
|
||||
- ⚠️ Added `GOTRUE_JWT_ISSUER` to Auth service configuration (requires `docker-compose.yml` update) - PR [#46020](https://github.com/supabase/supabase/pull/46020)
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.12` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.12)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.102.3` - [Release](https://github.com/supabase/realtime/releases/tag/v2.102.3)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.60.4` - [Release](https://github.com/supabase/storage/releases/tag/v1.60.4)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.96.6` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.96.6)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.74.0` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.74.0)
|
||||
|
||||
### Supavisor
|
||||
- Updated to `2.9.5` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.9.5)
|
||||
- Added `POSTGRES_HOST` to Supavisor service configuration (requires `docker-compose.yml` and `volumes/pooler/pooler.exs` update) - PR [#41273](https://github.com/supabase/supabase/pull/41273)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.43.1` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.43.1)
|
||||
- ⚠️ Changed default `docker-compose.yml` to no longer include logs & analytics. Read more in Supabase's [changelog](https://github.com/orgs/supabase/discussions/46084) - PR [#45327](https://github.com/supabase/supabase/pull/45327)
|
||||
|
||||
---
|
||||
|
||||
## 2026-04-27
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Added `docker-compose.envoy.yml` and `volumes/api/envoy`. See also the API gateway updates below - PR [#43838](https://github.com/supabase/supabase/pull/43838)
|
||||
- ⚠️ Changed Studio healthcheck and some other configuration for better compatibility with Podman (requires `docker-compose.yml` update) - PR [#44754](https://github.com/supabase/supabase/pull/44754)
|
||||
- ⚠️ Changed Studio configuration to bind to all IPv4 interfaces only (requires `docker-compose.yml` update) - PR [#44772](https://github.com/supabase/supabase/pull/44772)
|
||||
|
||||
### Documentation
|
||||
- Added a new [how-to](https://supabase.com/docs/guides/self-hosting/remove-superuser-access) describing how to switch from `supabase_admin` to `postgres` role for Studio - PR [#42975](https://github.com/supabase/supabase/pull/42975) (via [@singh-inder](https://github.com/singh-inder/))
|
||||
- Added a new [how-to](https://github.com/supabase/supabase/pull/45152) for configuring Envoy as the new API gateway - PR [#45152](https://github.com/supabase/supabase/pull/45152)
|
||||
- Updated the main [setup guide](https://supabase.com/docs/guides/self-hosting/docker) and the how-tos to reflect the state of the self-hosted Supabase configuration - PR [#45011](https://github.com/supabase/supabase/pull/45011)
|
||||
|
||||
### Utils
|
||||
- ⚠️ Added `utils/reassign-owner.sh` to update database objects. Read more in the "[Remove superuser access](https://supabase.com/docs/guides/self-hosting/remove-superuser-access)" how-to guide - PR [#42975](https://github.com/supabase/supabase/pull/42975)
|
||||
- ⚠️ Changed `utils/add-new-auth-keys.sh` to also update `docker-compose.yml` - PR [#45056](https://github.com/supabase/supabase/pull/45056)
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Added Envoy as the new optional API gateway (requires `docker-compose.envoy.yml`, `volumes/api/envoy`, and `volumes/logs/vector.yml` update) - PR [#43838](https://github.com/supabase/supabase/pull/43838) (via [@luizfelmach](https://github.com/luizfelmach/))
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.04.27-sha-5f60601`
|
||||
- ⚠️ Added 4 new lints to the Security Advisor. Read more about lint rules 0026 - 0029 in the [Performance and Security Advisors](https://supabase.com/docs/guides/database/database-advisors?queryGroups=lint&lint=0026_pg_graphql_anon_table_exposed) section of the Supabase documentation - PR [#45253](https://github.com/supabase/supabase/pull/45253), PR [#45260](https://github.com/supabase/supabase/pull/45260)
|
||||
---
|
||||
|
||||
## 2026-04-08
|
||||
|
||||
### Documentation
|
||||
- Added new how-to guides for configuring [custom email templates](https://supabase.com/docs/guides/self-hosting/custom-email-templates), setting up [SAML SSO](https://supabase.com/docs/guides/self-hosting/self-hosted-saml-sso), and [using Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) - PR [#42832](https://github.com/supabase/supabase/pull/42832), PR [#43386](https://github.com/supabase/supabase/pull/43386), PR [#44147](https://github.com/supabase/supabase/pull/44147)
|
||||
|
||||
### Utils
|
||||
- ⚠️ Added `utils/upgrade-pg17.sh`. Read more in the "[Upgrade to Postgres 17](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17)" how-to guide - PR [#44147](https://github.com/supabase/supabase/pull/44147)
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Added configuration for SAML SSO (requires `.env`, `docker-compose.yml` and `volumes/api/kong.yml` update) - PR [#43385](https://github.com/supabase/supabase/pull/43385) (via [@luizfelmach](https://github.com/luizfelmach/))
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.04.08-sha-205cbe7`
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.8` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.8)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.48.26` - [Release](https://github.com/supabase/storage/releases/tag/v1.48.26)
|
||||
|
||||
### imgproxy
|
||||
- Changed `IMGPROXY_ENABLE_WEBP_DETECTION` environment variable to `IMGPROXY_AUTO_WEBP` (requires `.env` and `docker-compose.yml` update) - PR [#43919](https://github.com/supabase/supabase/pull/43919)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.96.3` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.96.3)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.36.1` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.36.1)
|
||||
|
||||
### Postgres
|
||||
- ⚠️ Added `docker-compose.pg17.yml` override - PR [#44147](https://github.com/supabase/supabase/pull/44147)
|
||||
- ⚠️ Added `utils/upgrade-pg17.sh` - PR [#44147](https://github.com/supabase/supabase/pull/44147)
|
||||
- ⚠️ Added [documentation](https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17) explaining the upgrade to Postgres 17
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-16
|
||||
|
||||
⚠️ **Note:** This update includes **important changes**. Please check the details below. The following configuration files have been added/updated: `utils/add-new-auth-keys.sh`, `utils/rotate-new-api-keys.sh`, `docker-compose.yml`, `.env.example`, `docker-compose.s3.yml`, `docker-compose.rustfs.yml`, `volumes/api/kong.yml`, `volumes/api/kong-entrypoint.sh`, `docker-compose.caddy.yml`, `docker-compose.nginx.yml`, `volumes/functions/main/index.ts`, and `volumes/proxy`.
|
||||
|
||||
### Configuration
|
||||
- ⚠️ Added scripts and templates to support the new API key format (`sb_` API keys) and the new asymmetric authentication. Check the [how-to guide](https://supabase.com/docs/guides/self-hosting/self-hosted-auth-keys) for detailed instructions - PR [#43554](https://github.com/supabase/supabase/pull/43554)
|
||||
- Added optional proxy configuration for Caddy and nginx. Read the [how-to guide](https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https) to learn more - PR [#43291](https://github.com/supabase/supabase/pull/43291)
|
||||
|
||||
### Documentation
|
||||
- Added several new how-to guides to the self-hosted Supabase [documentation](https://supabase.com/docs/guides/self-hosting) - PR [#42745](https://github.com/supabase/supabase/pull/42745), PR [#42953](https://github.com/supabase/supabase/pull/42953), PR [#43177](https://github.com/supabase/supabase/pull/43177), PR [#43286](https://github.com/supabase/supabase/pull/43286), PR [#43293](https://github.com/supabase/supabase/pull/43293)
|
||||
|
||||
### Utils and tests
|
||||
- Added `utils/add-new-auth-keys.sh` and `utils/rotate-new-api-keys.sh` - PR [#43554](https://github.com/supabase/supabase/pull/43554)
|
||||
- Added `tests/` with 100+ test cases - PR [#43573](https://github.com/supabase/supabase/pull/43573)
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.03.16-sha-5528817`
|
||||
- ⚠️ Added the link to the Data API page in Integrations - PR [#43268](https://github.com/supabase/supabase/pull/43268)
|
||||
- ⚠️ Added `PGRST_DB_SCHEMAS`, `PGRST_DB_EXTRA_SEARCH_PATH`, and `PGRST_DB_MAX_ROWS` to Studio configuration (requires `docker-compose.yml` update) - PR [#43268](https://github.com/supabase/supabase/pull/43268)
|
||||
|
||||
### MCP Server
|
||||
- Updated to `v0.7.0` - [Release](https://github.com/supabase/mcp/releases/tag/v0.7.0)
|
||||
|
||||
### API gateway
|
||||
- ⚠️ Updated Kong to `3.9.1` - PR [#43554](https://github.com/supabase/supabase/pull/43554)
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.6` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.6)
|
||||
|
||||
### Realtime
|
||||
- ⚠️ Added **mandatory** `METRICS_JWT_SECRET` environment variable (requires `docker-compose.s3.yml` update) - PR [realtime#1729](https://github.com/supabase/realtime/pull/1729)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.44.2` - [Release](https://github.com/supabase/storage/releases/tag/v1.44.2)
|
||||
- ⚠️ Added `STORAGE_PUBLIC_URL` environment variable to simplify proxy configuration (requires `docker-compose.s3.yml` update) - PR [storage#900](https://github.com/supabase/storage/pull/900)
|
||||
- ⚠️ Added RustFS as an optional S3 backend - PR [#42935](https://github.com/supabase/supabase/pull/42935)
|
||||
- ⚠️ Changed Docker Compose configuration for S3 backends to use named volumes - PR [#43815](https://github.com/supabase/supabase/pull/43815)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.71.2` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.71.2)
|
||||
- ⚠️ Added `SUPABASE_PUBLISHABLE_KEYS`, `SUPABASE_SECRET_KEYS`, and `SUPABASE_PUBLIC_URL` environment variables (requires `docker-compose.yml` update)
|
||||
- ⚠️ Added an option for a "hybrid" JWT verification following the addition of the new API keys and the new asymmetric authentication (requires `volumes/functions/main/index.ts` update) - PR [#42130](https://github.com/supabase/supabase/pull/42130)
|
||||
- ⚠️ Added optional rate limiter - PR [edge-runtime#670](https://github.com/supabase/edge-runtime/pull/670)
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-18
|
||||
|
||||
### Storage
|
||||
- Changed MinIO image to use Chainguard [minio](https://images.chainguard.dev/directory/image/minio/overview) and [minio-client](https://images.chainguard.dev/directory/image/minio-client/overview) (requires `docker-compose.s3.yml` update) - PR [#42942](https://github.com/supabase/supabase/pull/42942)
|
||||
- Updated Storage image version to `v1.37.8` in `docker-compose.s3.yml`
|
||||
- Removed `imgproxy` service from `docker-compose.s3.yml` to minimize redundancy - PR [#42942](https://github.com/supabase/supabase/pull/42942)
|
||||
- Fixed inconsistent `storage` service entry ordering in `docker-compose.yml` and `docker-compose.s3.yml` to improve diff readability - PR [#42942](https://github.com/supabase/supabase/pull/42942)
|
||||
|
||||
### Edge Runtime
|
||||
- Added a `deno-cache` named volume to avoid re-downloading dependencies (requires `docker-compose.yml` and `volumes/functions/*` update) - PR [#40822](https://github.com/supabase/supabase/pull/40822)
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-16
|
||||
|
||||
⚠️ **Note:** This update includes several breaking changes, including a security fix for Analytics. Please check the details below. The following configuration files have been updated: `docker-compose.yml`, `.env.example`, `docker-compose.s3.yml`, `volumes/api/kong.yml`, and `volumes/logs/vector.yml`.
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.02.16-sha-26c615c`
|
||||
- Added Edge Functions management UI (requires `docker-compose.yml` update) - PR [#40690](https://github.com/supabase/supabase/pull/40690), PR [#42322](https://github.com/supabase/supabase/pull/42322), PR [#42349](https://github.com/supabase/supabase/pull/42349), PR [#42350](https://github.com/supabase/supabase/pull/42350)
|
||||
|
||||
### MCP Server
|
||||
- Updated to `v0.6.3` - [Release](https://github.com/supabase/mcp/releases/tag/v0.6.3)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.186.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.186.0)
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.5` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.5)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.76.5` - [Release](https://github.com/supabase/realtime/releases/tag/v2.76.5)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.37.8` - [Release](https://github.com/supabase/storage/releases/tag/v1.37.8)
|
||||
- ⚠️ Changed environment variable configuration for Storage (requires `docker-compose.yml`, `.env.example` and `.env` update) - PR [#37185](https://github.com/supabase/supabase/pull/37185), PR [#42862](https://github.com/supabase/supabase/pull/42862)
|
||||
- ⚠️ Added **default** configuration to access buckets via `/storage/v1/s3` endpoint (requires `docker-compose.yml` and `.env` update) - PR [#37185](https://github.com/supabase/supabase/pull/37185)
|
||||
- ⚠️ Changed MinIO configuration for the S3 backend (requires `docker-compose.s3.yml` and `.env` update) - PR [#37185](https://github.com/supabase/supabase/pull/37185)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.70.3` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.70.3)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.31.2` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.31.2)
|
||||
- ⚠️ Changed default configuration to disable Logflare on `0.0.0.0:4000` to prevent access to `/dashboard` (requires `docker-compose.yml` update). Read more in the "Production Recommendations" section of Logflare [documentation](https://supabase.com/docs/reference/self-hosting-analytics/introduction) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
|
||||
- ⚠️ Changed Kong routes to not include `/analytics/v1` by default (requires `/volumes/api/kong.yml` update) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
|
||||
|
||||
### Vector
|
||||
- Updated to `0.53.0-alpine` - [Changelog](https://vector.dev/releases/0.53.0/) | [Release](https://github.com/vectordotdev/vector/releases/tag/v0.53.0)
|
||||
- ⚠️ Major version jump from `0.28.1` (requires `volumes/logs/vector.yml` update) - PR [#42525](https://github.com/supabase/supabase/pull/42525)
|
||||
- ⚠️ Changed Postgres sink configuration to bypass Kong (requires `volumes/logs/vector.yml` update) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
|
||||
- ⚠️ Changed retry settings for all sinks to increase timeouts (requires `volumes/logs/vector.yml` update) - PR [#42857](https://github.com/supabase/supabase/pull/42857)
|
||||
|
||||
---
|
||||
|
||||
## 2026-02-05
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.37.1` - [Release](https://github.com/supabase/storage/releases/tag/v1.37.1)
|
||||
- Fixed an issue with Storage not starting because of an issue with migrations - PR [storage#845](https://github.com/supabase/storage/pull/845)
|
||||
|
||||
---
|
||||
|
||||
## 2026-01-27
|
||||
|
||||
### Studio
|
||||
- Updated to `2026.01.27-sha-6aa59ff`
|
||||
- Added SQL snippets (requires `docker-compose.yml` update) - PR [#41112](https://github.com/supabase/supabase/pull/41112), PR [#41557](https://github.com/supabase/supabase/pull/41557), discussion [#42031](https://github.com/orgs/supabase/discussions/42031)
|
||||
- Fixed type generator - PR [#40481](https://github.com/supabase/supabase/pull/40481)
|
||||
- Fixed minor UI discrepancies - PR [#40579](https://github.com/supabase/supabase/pull/40579), PR [#41936](https://github.com/supabase/supabase/pull/41936), PR [#41970](https://github.com/supabase/supabase/pull/41970), PR [#41971](https://github.com/supabase/supabase/pull/41971), PR [#41972](https://github.com/supabase/supabase/pull/41972), PR [#42015](https://github.com/supabase/supabase/pull/42015)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.185.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.185.0)
|
||||
- ⚠️ Fixed security-related issues
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.3` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.3)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.72.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.72.0)
|
||||
- Changed healthchecks logging to off by default (requires `docker-compose.yml` update) - PR [realtime#1677](https://github.com/supabase/realtime/pull/1677), PR [#42156](https://github.com/supabase/supabase/pull/42156)
|
||||
- Changed logging configuration and healthcheck frequency to reduce log volume (requires `docker-compose.yml` update) - PR [#42112](https://github.com/supabase/supabase/pull/42112)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.33.5` - [Release](https://github.com/supabase/storage/releases/tag/v1.33.5)
|
||||
|
||||
### imgproxy
|
||||
- Updated to `v3.30.1` - [Changelog](https://github.com/imgproxy/imgproxy/blob/master/CHANGELOG.md) | [Release](https://github.com/imgproxy/imgproxy/releases/tag/v3.30.1)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.95.2` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.95.2)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.70.0` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.70.0)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.30.3` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.30.3)
|
||||
|
||||
### Postgres
|
||||
- No image update
|
||||
- Fixed Postgres logging configuration (requires `volumes/logs/vector.yml` update) - PR [#41800](https://github.com/supabase/supabase/pull/41800)
|
||||
|
||||
---
|
||||
|
||||
## 2025-12-18
|
||||
|
||||
### Documentation
|
||||
- Updated self-hosting installation and configuration guide - PR [#40901](https://github.com/supabase/supabase/pull/40901), PR [#41438](https://github.com/supabase/supabase/pull/41438)
|
||||
|
||||
### Utils
|
||||
- Added `utils/generate-keys.sh` - PR [#41363](https://github.com/supabase/supabase/pull/41363)
|
||||
- Added `utils/db-passwd.sh` - PR [#41432](https://github.com/supabase/supabase/pull/41432)
|
||||
- Changed `reset.sh` to POSIX and added more checks - PR [#41361](https://github.com/supabase/supabase/pull/41361)
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.12.17-sha-43f4f7f`
|
||||
- ⚠️ Fixed additional issues related to [React2Shell](https://vercel.com/kb/bulletin/react2shell)
|
||||
- Fixed an issue with the Users page not being updated on changes - PR [#41254](https://github.com/supabase/supabase/pull/41254)
|
||||
|
||||
### MCP Server
|
||||
- Updated to `v0.5.10` - [Release](https://github.com/supabase/mcp/releases/tag/v0.5.10)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.184.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.184.0)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.95.1` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.95.1)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.27.0` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.27.0)
|
||||
- Fixed multiple issues, including a race condition
|
||||
|
||||
---
|
||||
|
||||
## 2025-12-10
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.12.09-sha-434634f`
|
||||
- ⚠️ Fixed security issues related to [React2Shell](https://vercel.com/kb/bulletin/react2shell)
|
||||
|
||||
### MCP Server
|
||||
- Updated to `v0.5.9` - [Release](https://github.com/supabase/mcp/releases/tag/v0.5.9)
|
||||
- ⚠️ Changed MCP tool `get_anon_key` to `get_publishable_keys`
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v14.1` - [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md) | [Release](https://github.com/PostgREST/postgrest/releases/tag/v14.1)
|
||||
- ⚠️ **Major upgrade from v13.x to v14.x** - please report any unexpected behavior
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.68.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.68.0)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.33.0` - [Release](https://github.com/supabase/storage/releases/tag/v1.33.0)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.28` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.28)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.26.25` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.25)
|
||||
|
||||
---
|
||||
|
||||
## 2025-12-08
|
||||
|
||||
### Realtime
|
||||
- No image update
|
||||
- Changed boolean values to strings in Docker Compose for better compatibility with Podman - PR [#40994](https://github.com/supabase/supabase/pull/40994), also PR [realtime#1614](https://github.com/supabase/realtime/pull/1614)
|
||||
- Changed healthcheck in Docker Compose for better compatibility with Podman - PR [#41159](https://github.com/supabase/supabase/pull/41159)
|
||||
|
||||
---
|
||||
|
||||
## 2025-11-26
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.11.26-sha-8f096b5`
|
||||
- Fixed MCP `get_advisors` tool - PR [#40783](https://github.com/supabase/supabase/pull/40783)
|
||||
- Fixed AI Assistant request schema - PR [#40830](https://github.com/supabase/supabase/pull/40830)
|
||||
- Fixed log drains page - PR [#40835](https://github.com/supabase/supabase/pull/40835)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.65.3` - [Release](https://github.com/supabase/realtime/releases/tag/v2.65.3)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.26.13` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.13)
|
||||
- Fixed crashdump when `POSTGRES_BACKEND_URL` is malformed - PR [logflare#2954](https://github.com/Logflare/logflare/pull/2954)
|
||||
|
||||
---
|
||||
|
||||
## 2025-11-25
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.11.24-sha-d990ae8` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40734)
|
||||
- Fixed Queues configuration UI and added [documentation for exposed queue schema](https://supabase.com/docs/guides/queues/expose-self-hosted-queues) - PR [#40078](https://github.com/supabase/supabase/pull/40078)
|
||||
- Fixed parameterized SQL queries in MCP tools - PR [#40499](https://github.com/supabase/supabase/pull/40499)
|
||||
- Fixed Studio showing paid options for log drains - PR [#40510](https://github.com/supabase/supabase/pull/40510)
|
||||
- Fixed AI Assistant authentication - PR [#40654](https://github.com/supabase/supabase/pull/40654)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.183.0` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md) | [Release](https://github.com/supabase/auth/releases/tag/v2.183.0)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.65.2` - [Release](https://github.com/supabase/realtime/releases/tag/v2.65.2)
|
||||
- Fixed handling of boolean configuration options - PR [realtime#1614](https://github.com/supabase/realtime/pull/1614)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.32.0` - [Release](https://github.com/supabase/storage/releases/tag/v1.32.0)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.25` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.25)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.26.12` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.26.12)
|
||||
- Fixed Auth logs query - PR [logflare#2936](https://github.com/Logflare/logflare/pull/2936)
|
||||
- Fixed build configuration to prevent crashes with "Illegal instruction (core dumped)" - PR [logflare#2942](https://github.com/Logflare/logflare/pull/2942)
|
||||
|
||||
---
|
||||
|
||||
## 2025-11-17
|
||||
|
||||
### Storage
|
||||
- No image update
|
||||
- Fixed resumable uploads for files larger than 6MB (requires `docker-compose.yml` update) - PR [#40500](https://github.com/supabase/supabase/pull/40500)
|
||||
|
||||
---
|
||||
|
||||
## 2025-11-12
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.11.10-sha-5291fe3` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40083)
|
||||
- Added log drains - PR [#28297](https://github.com/supabase/supabase/pull/28297)
|
||||
- Fixed Studio using `postgres` role instead of `supabase_admin` - PR [#39946](https://github.com/supabase/supabase/pull/39946)
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.182.1` - [Changelog](https://github.com/supabase/auth/blob/master/CHANGELOG.md#21821-2025-11-05) | [Release](https://github.com/supabase/auth/releases/tag/v2.182.1)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.63.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.63.0)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.29.0` - [Release](https://github.com/supabase/storage/releases/tag/v1.29.0)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.23` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.23)
|
||||
|
||||
### Supavisor
|
||||
- Updated to `2.7.4` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.4)
|
||||
|
||||
---
|
||||
|
||||
## 2025-11-05
|
||||
|
||||
### Studio
|
||||
- No image update
|
||||
- Fixed Studio failing to connect to Postgres with non-default settings (requires `docker-compose.yml` update) - PR [#40169](https://github.com/supabase/supabase/pull/40169)
|
||||
|
||||
### Realtime
|
||||
- No image update
|
||||
- Fixed realtime logs not showing in Studio (requires `volumes/logs/vector.yml` update) - PR [#39963](https://github.com/supabase/supabase/pull/39963)
|
||||
|
||||
---
|
||||
|
||||
## 2025-10-28
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.10.27-sha-85b84e0` - [Dashboard updates](https://github.com/orgs/supabase/discussions/40083)
|
||||
- Fixed broken authentication when uploading files to Storage - PR [#39829](https://github.com/supabase/supabase/pull/39829)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.57.2` - [Release](https://github.com/supabase/realtime/releases/tag/v2.57.2)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.28.2` - [Release](https://github.com/supabase/storage/releases/tag/v1.28.2)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.93.1` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.93.1)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.15` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.15)
|
||||
|
||||
---
|
||||
|
||||
## 2025-10-27
|
||||
|
||||
### Studio
|
||||
- No image update
|
||||
- Added Kong configuration for MCP server routes (requires `volumes/api/kong.yml` update) - PR [#39849](https://github.com/supabase/supabase/pull/39849)
|
||||
- Added [documentation page](https://supabase.com/docs/guides/self-hosting/enable-mcp) for MCP server configuration - PR [#39952](https://github.com/supabase/supabase/pull/39952)
|
||||
|
||||
---
|
||||
|
||||
## 2025-10-21
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.10.20-sha-5005fc6` - [Dashboard updates](https://github.com/orgs/supabase/discussions/39709)
|
||||
- Fixed issues with Edge Functions and cron logs not being visible in Studio - PR [#39388](https://github.com/supabase/supabase/pull/39388), PR [#39704](https://github.com/supabase/supabase/pull/39704), PR [#39711](https://github.com/supabase/supabase/pull/39711)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.56.0` - [Release](https://github.com/supabase/realtime/releases/tag/v2.56.0)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.28.1` - [Release](https://github.com/supabase/storage/releases/tag/v1.28.1)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.93.0` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.93.0)
|
||||
|
||||
### Edge Runtime
|
||||
- Updated to `v1.69.14` - [Release](https://github.com/supabase/edge-runtime/releases/tag/v1.69.14)
|
||||
|
||||
### Supavisor
|
||||
- Updated to `2.7.3` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.3)
|
||||
|
||||
---
|
||||
|
||||
## 2025-10-13
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.22.6` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.6)
|
||||
|
||||
---
|
||||
|
||||
## 2025-10-08
|
||||
|
||||
### Studio
|
||||
- Updated to `2025.10.01-sha-8460121` - [Dashboard updates](https://github.com/orgs/supabase/discussions/39709)
|
||||
- Added "local" remote MCP server - PR [#38797](https://github.com/supabase/supabase/pull/38797), PR [#39041](https://github.com/supabase/supabase/pull/39041)
|
||||
- ⚠️ Changed Studio connection method to `postgres-meta` - affects non-standard database port configurations
|
||||
|
||||
### Auth
|
||||
- Updated to `v2.180.0` - [Release](https://github.com/supabase/auth/releases/tag/v2.180.0)
|
||||
|
||||
### PostgREST
|
||||
- Updated to `v13.0.7` - [Release](https://github.com/PostgREST/postgrest/releases/tag/v13.0.7) | [Changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md)
|
||||
|
||||
### Realtime
|
||||
- Updated to `v2.51.11` - [Release](https://github.com/supabase/realtime/releases/tag/v2.51.11)
|
||||
|
||||
### Storage
|
||||
- Updated to `v1.28.0` - [Release](https://github.com/supabase/storage/releases/tag/v1.28.0)
|
||||
|
||||
### Postgres Meta
|
||||
- Updated to `v0.91.6` - [Release](https://github.com/supabase/postgres-meta/releases/tag/v0.91.6)
|
||||
|
||||
### Analytics (Logflare)
|
||||
- Updated to `1.22.4` - [Release](https://github.com/Logflare/logflare/releases/tag/v1.22.4)
|
||||
|
||||
### Postgres
|
||||
- Updated to `15.8.1.085` - [Release](https://github.com/supabase/postgres/releases/tag/15.8.1.085)
|
||||
|
||||
### Supavisor
|
||||
- Updated to `2.7.0` - [Release](https://github.com/supabase/supavisor/releases/tag/v2.7.0)
|
||||
|
||||
---
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
# Reverse proxy di produzione, TLS automatico via Let's Encrypt.
|
||||
# Copia in Caddyfile, sostituisci il dominio, poi:
|
||||
# caddy run --config Caddyfile
|
||||
# (o come container: docker run -p 80:80 -p 443:443 -v ./Caddyfile:/etc/caddy/Caddyfile caddy)
|
||||
#
|
||||
# Un solo hostname: /api/* va al gateway Supabase (envoy accetta qualsiasi Host, instrada per
|
||||
# path), /cms/* va al CMS Strapi (infra/strapi/), tutto il resto va all'app. handle_path toglie
|
||||
# il prefisso prima di inoltrare, quindi VITE_SUPABASE_URL nell'app deve essere
|
||||
# https://<dominio>/api (supabase-js aggiunge da solo /rest/v1, /auth/v1, ecc.) e VITE_STRAPI_URL
|
||||
# deve essere https://<dominio>/cms.
|
||||
|
||||
crapp.ddns.net {
|
||||
handle_path /api/* {
|
||||
reverse_proxy 127.0.0.1:8000
|
||||
}
|
||||
handle_path /cms/* {
|
||||
reverse_proxy 127.0.0.1:1337
|
||||
}
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:3000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<div align="center">
|
||||
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://deepwiki.com/supabase/supabase/3-self-hosted-deployment)
|
||||
|
||||
</div>
|
||||
|
||||
# Self-Hosted Supabase with Docker
|
||||
|
||||
This is the official Docker Compose setup for self-hosted Supabase. It provides a complete stack with all Supabase services running locally or on your infrastructure.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Follow the detailed setup guide in our documentation: [Self-Hosting with Docker](https://supabase.com/docs/guides/self-hosting/docker)
|
||||
|
||||
The guide covers:
|
||||
- Prerequisites (Git and Docker)
|
||||
- Initial setup and configuration
|
||||
- Securing your installation
|
||||
- Accessing services
|
||||
- Updating your instance
|
||||
|
||||
## What's Included
|
||||
|
||||
This Docker Compose configuration includes the following services:
|
||||
|
||||
- **[Studio](https://github.com/supabase/supabase/tree/master/apps/studio)** - A dashboard for managing your self-hosted Supabase project
|
||||
- **[Envoy](https://www.envoyproxy.io/)** - API gateway (default; Kong is available as an optional override via `sh run.sh config add kong`)
|
||||
- **[Auth](https://github.com/supabase/auth)** - JWT-based authentication API for user sign-ups, logins, and session management
|
||||
- **[PostgREST](https://github.com/PostgREST/postgrest)** - Web server that turns your PostgreSQL database directly into a RESTful API
|
||||
- **[Realtime](https://github.com/supabase/realtime)** - Elixir server that listens to PostgreSQL database changes and broadcasts them over websockets
|
||||
- **[Storage](https://github.com/supabase/storage)** - RESTful API for managing files in S3, with Postgres handling permissions
|
||||
- **[imgproxy](https://github.com/imgproxy/imgproxy)** - Fast and secure image processing server
|
||||
- **[postgres-meta](https://github.com/supabase/postgres-meta)** - RESTful API for managing Postgres (fetch tables, add roles, run queries)
|
||||
- **[PostgreSQL](https://github.com/supabase/postgres)** - Object-relational database with over 30 years of active development
|
||||
- **[Edge Runtime](https://github.com/supabase/edge-runtime)** - Web server based on Deno runtime for running JavaScript, TypeScript, and WASM services
|
||||
- **[Logflare](https://github.com/Logflare/logflare)** - Log management and event analytics platform
|
||||
- **[Vector](https://github.com/vectordotdev/vector)** - High-performance observability data pipeline for logs
|
||||
- **[Supavisor](https://github.com/supabase/supavisor)** - Supabase's Postgres connection pooler
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Self-Hosting with Docker](https://supabase.com/docs/guides/self-hosting/docker)** - Setup and configuration guides
|
||||
- **[CHANGELOG.md](./CHANGELOG.md)** - Track recent updates and changes to services
|
||||
- **[versions.md](./versions.md)** - Complete history of Docker image versions for rollback reference
|
||||
- **[Ask DeepWiki / Supabase](https://deepwiki.com/supabase/supabase/3-self-hosted-deployment)** - DeepWiki-generated description of self-hosted configuration
|
||||
- **[CONFIG.md](./CONFIG.md)** - Configuration reference for all environment variables
|
||||
- **[Update your deployment](https://supabase.com/docs/guides/self-hosting/updating)** - Update an existing deployment with `update.sh`
|
||||
|
||||
## Updates
|
||||
|
||||
Back up your database, then:
|
||||
|
||||
```sh
|
||||
sh update.sh --dry-run # optional preview
|
||||
sh update.sh
|
||||
sh run.sh pull && sh run.sh recreate
|
||||
```
|
||||
|
||||
See the **[update guide](https://supabase.com/docs/guides/self-hosting/updating)** for conflicts,
|
||||
breaking changes, pinning a release, and older installs without `.supabase-version`.
|
||||
|
||||
## Community & Support
|
||||
|
||||
For troubleshooting common issues, see:
|
||||
- [GitHub Discussions](https://github.com/orgs/supabase/discussions?discussions_q=is%3Aopen+label%3Aself-hosted) - Questions, feature requests, and workarounds
|
||||
- [GitHub Issues](https://github.com/supabase/supabase/issues?q=is%3Aissue%20state%3Aopen%20label%3Aself-hosted) - Known issues
|
||||
- [Documentation](https://supabase.com/docs/guides/self-hosting) - Setup and configuration guides
|
||||
|
||||
Self-hosted Supabase is community-supported. Get help and connect with other users:
|
||||
|
||||
- [Discord](https://discord.supabase.com) - Real-time chat and community support
|
||||
- [Reddit](https://www.reddit.com/r/Supabase/) - Official Supabase subreddit
|
||||
|
||||
Share your self-hosting experience:
|
||||
|
||||
- [GitHub Discussions](https://github.com/orgs/supabase/discussions/39820) - "Self-hosting: What's working (and what's not)?"
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Security
|
||||
|
||||
⚠️ **The default configuration is not secure for production use.**
|
||||
|
||||
Before deploying to production, you must:
|
||||
- [Update](https://supabase.com/docs/guides/self-hosting/docker#configuring-and-securing-supabase) all default passwords and secrets in the `.env` file
|
||||
- Review and update CORS settings
|
||||
- Consider setting up a secure proxy in front of self-hosted Supabase
|
||||
- Review and adjust network security configuration (ACLs, etc.)
|
||||
- Set up proper backup procedures
|
||||
|
||||
See the [main installation guide](https://supabase.com/docs/guides/self-hosting/docker) and the how-tos in the documentation.
|
||||
|
||||
## License
|
||||
|
||||
This repository is licensed under the Apache 2.0 License. See the main [Supabase repository](https://github.com/supabase/supabase) for details.
|
||||
@@ -0,0 +1,48 @@
|
||||
create table profiles (
|
||||
id uuid references auth.users not null,
|
||||
updated_at timestamp with time zone,
|
||||
username text unique,
|
||||
avatar_url text,
|
||||
website text,
|
||||
|
||||
primary key (id),
|
||||
unique(username),
|
||||
constraint username_length check (char_length(username) >= 3)
|
||||
);
|
||||
|
||||
alter table profiles enable row level security;
|
||||
|
||||
create policy "Public profiles are viewable by the owner."
|
||||
on profiles for select
|
||||
using ( auth.uid() = id );
|
||||
|
||||
create policy "Users can insert their own profile."
|
||||
on profiles for insert
|
||||
with check ( auth.uid() = id );
|
||||
|
||||
create policy "Users can update own profile."
|
||||
on profiles for update
|
||||
using ( auth.uid() = id );
|
||||
|
||||
-- Set up Realtime
|
||||
begin;
|
||||
drop publication if exists supabase_realtime;
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
alter publication supabase_realtime add table profiles;
|
||||
|
||||
-- Set up Storage
|
||||
insert into storage.buckets (id, name)
|
||||
values ('avatars', 'avatars');
|
||||
|
||||
create policy "Avatar images are publicly accessible."
|
||||
on storage.objects for select
|
||||
using ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can upload an avatar."
|
||||
on storage.objects for insert
|
||||
with check ( bucket_id = 'avatars' );
|
||||
|
||||
create policy "Anyone can update an avatar."
|
||||
on storage.objects for update
|
||||
with check ( bucket_id = 'avatars' );
|
||||
@@ -0,0 +1,44 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
studio:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: apps/studio/Dockerfile
|
||||
target: dev
|
||||
ports:
|
||||
- 8082:8082
|
||||
develop:
|
||||
watch:
|
||||
- action: sync
|
||||
path: ../apps/studio
|
||||
target: /app/apps/studio
|
||||
ignore:
|
||||
- node_modules/
|
||||
- action: rebuild
|
||||
path: package.json
|
||||
|
||||
mail:
|
||||
container_name: supabase-mail
|
||||
image: inbucket/inbucket:3.0.3
|
||||
ports:
|
||||
- '2500:2500' # SMTP
|
||||
- '9000:9000' # web interface
|
||||
- '1100:1100' # POP3
|
||||
auth:
|
||||
environment:
|
||||
- GOTRUE_SMTP_USER=
|
||||
- GOTRUE_SMTP_PASS=
|
||||
meta:
|
||||
ports:
|
||||
- 5555:8080
|
||||
db:
|
||||
restart: 'no'
|
||||
volumes:
|
||||
# Always use a fresh database when developing
|
||||
- /var/lib/postgresql/data
|
||||
# Seed data should be inserted last (alphabetical order)
|
||||
- ./dev/data.sql:/docker-entrypoint-initdb.d/seed.sql
|
||||
storage:
|
||||
volumes:
|
||||
- /var/lib/storage
|
||||
@@ -0,0 +1,42 @@
|
||||
services:
|
||||
|
||||
# Caddy terminates TLS and forwards to the API gateway (api-gw) on port 8000,
|
||||
# so the gateway's own host port binding is removed here. This works for
|
||||
# either gateway, since the service is named api-gw in both cases.
|
||||
api-gw:
|
||||
ports: !reset []
|
||||
# When using the Kong override uncomment the following:
|
||||
#environment:
|
||||
# KONG_PORT_MAPS: "443:8000,443:8443"
|
||||
|
||||
caddy:
|
||||
container_name: supabase-caddy
|
||||
image: caddy:2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
depends_on:
|
||||
api-gw:
|
||||
condition: service_healthy
|
||||
studio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PROXY_DOMAIN: ${PROXY_DOMAIN}
|
||||
PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME}
|
||||
PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
PROXY_AUTH_PASSWORD=$$(caddy hash-password --plaintext "$$PROXY_AUTH_PASSWORD") && \
|
||||
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
||||
volumes:
|
||||
- ./volumes/proxy/caddy:/etc/caddy
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
@@ -0,0 +1,15 @@
|
||||
# DEPRECATED: Envoy is now the default API gateway defined directly in
|
||||
# docker-compose.yml, so this override is no longer needed and does nothing.
|
||||
#
|
||||
# This no-op shim is kept for one release cycle so existing COMPOSE_FILE
|
||||
# entries referencing it do not break. Remove it from your configuration:
|
||||
#
|
||||
# sh run.sh config remove envoy
|
||||
#
|
||||
# To run Kong instead of Envoy, use the Kong override:
|
||||
#
|
||||
# sh run.sh config add kong
|
||||
#
|
||||
# This file will be removed in a future release.
|
||||
|
||||
services: {}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Kong API gateway override.
|
||||
#
|
||||
# Replaces the default Envoy gateway with Kong. Enable with:
|
||||
# sh run.sh config add kong
|
||||
# sh run.sh start
|
||||
#
|
||||
# This overrides the `api-gw` service in place, so its dependents (functions),
|
||||
# the reverse-proxy overrides (caddy/nginx), and the `kong`/`envoy` network
|
||||
# aliases keep working unchanged. Kong re-adds an HTTPS listener on 8443.
|
||||
|
||||
services:
|
||||
api-gw:
|
||||
container_name: supabase-kong
|
||||
image: kong/kong:3.9.3
|
||||
healthcheck:
|
||||
test: !override ["CMD", "kong", "health"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
ports: !override
|
||||
- ${API_GW_HTTP_PORT:-${KONG_HTTP_PORT:-8000}}:8000/tcp
|
||||
- ${KONG_HTTPS_PORT:-8443}:8443/tcp
|
||||
volumes: !override
|
||||
- ./volumes/api/kong.yml:/home/kong/temp.yml:ro,z
|
||||
- ./volumes/api/kong-entrypoint.sh:/home/kong/kong-entrypoint.sh:ro,z
|
||||
#- ./volumes/api/server.crt:/home/kong/server.crt:ro
|
||||
#- ./volumes/api/server.key:/home/kong/server.key:ro
|
||||
environment: !override
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /usr/local/kong/kong.yml
|
||||
KONG_ROUTER_FLAVOR: expressions
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
KONG_DNS_NOT_FOUND_TTL: 1
|
||||
KONG_DNS_VALID_TTL: 5
|
||||
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth,request-termination,ip-restriction,post-function
|
||||
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
||||
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
||||
KONG_PROXY_ACCESS_LOG: /dev/stdout combined
|
||||
#KONG_SSL_CERT: /home/kong/server.crt
|
||||
#KONG_SSL_CERT_KEY: /home/kong/server.key
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-}
|
||||
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-}
|
||||
ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-}
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-}
|
||||
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
|
||||
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||
entrypoint: !override ["/bin/sh", "/home/kong/kong-entrypoint.sh"]
|
||||
@@ -0,0 +1,99 @@
|
||||
# This override adds the following to the self-hosted Supabase configuration:
|
||||
# - Logflare: Log management and event analytics platform
|
||||
# - Vector: High-performance observability data pipeline for logs
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.logs.yml up -d
|
||||
|
||||
services:
|
||||
|
||||
studio:
|
||||
depends_on:
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
|
||||
LOGFLARE_URL: http://analytics:4000
|
||||
ENABLED_FEATURES_LOGS_ALL: "true"
|
||||
|
||||
analytics:
|
||||
container_name: supabase-analytics
|
||||
image: supabase/logflare:1.43.1
|
||||
restart: unless-stopped
|
||||
#ports:
|
||||
# - 4000:4000
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -sSfL -o /dev/null http://localhost:4000/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
environment:
|
||||
LOGFLARE_NODE_HOST: 127.0.0.1
|
||||
|
||||
DB_USERNAME: supabase_admin
|
||||
DB_DATABASE: _supabase
|
||||
DB_HOSTNAME: ${POSTGRES_HOST}
|
||||
DB_PORT: ${POSTGRES_PORT}
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_SCHEMA: _analytics
|
||||
|
||||
# Enable single-tenant mode for Logflare
|
||||
LOGFLARE_SINGLE_TENANT: "true"
|
||||
# Seed Supabase-related metadata
|
||||
LOGFLARE_SUPABASE_MODE: "true"
|
||||
|
||||
LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
|
||||
LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN}
|
||||
|
||||
LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true
|
||||
|
||||
# Comment out the following two variables when switching to
|
||||
# the BigQuery backend for logs
|
||||
POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
|
||||
POSTGRES_BACKEND_SCHEMA: _analytics
|
||||
|
||||
# Uncomment to use the BigQuery backend for logs
|
||||
#GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID}
|
||||
#GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER}
|
||||
# Uncomment to use the BigQuery backend for logs (requires gcloud.json
|
||||
# service account key from Google Cloud Console)
|
||||
#volumes:
|
||||
# - ./gcloud.json:/opt/app/rel/logflare/bin/gcloud.json:ro,z
|
||||
|
||||
vector:
|
||||
container_name: supabase-vector
|
||||
image: timberio/vector:0.53.0-alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z
|
||||
- ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"wget --no-verbose --tries=1 --spider http://vector:9001/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN}
|
||||
command:
|
||||
[
|
||||
"--config",
|
||||
"/etc/vector/vector.yml"
|
||||
]
|
||||
security_opt:
|
||||
- "label=disable"
|
||||
@@ -0,0 +1,41 @@
|
||||
services:
|
||||
|
||||
# nginx terminates TLS and forwards to the API gateway (api-gw) on port 8000,
|
||||
# so the gateway's own host port binding is removed here. This works for
|
||||
# either gateway, since the service is named api-gw in both cases.
|
||||
api-gw:
|
||||
ports: !reset []
|
||||
# When using the Kong override, uncomment the following:
|
||||
#environment:
|
||||
# KONG_PORT_MAPS: "443:8000,443:8443"
|
||||
|
||||
nginx:
|
||||
container_name: supabase-nginx
|
||||
image: jonasal/nginx-certbot:6.2.0-nginx1.31.3
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
depends_on:
|
||||
api-gw:
|
||||
condition: service_healthy
|
||||
studio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PROXY_DOMAIN: ${PROXY_DOMAIN}
|
||||
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
|
||||
PROXY_AUTH_USERNAME: ${DASHBOARD_USERNAME}
|
||||
PROXY_AUTH_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
- |
|
||||
printf '%s:%s\n' "$${PROXY_AUTH_USERNAME}" "$$(openssl passwd -apr1 "$${PROXY_AUTH_PASSWORD}")" > /etc/nginx/user_conf.d/dashboard-passwd && \
|
||||
envsubst '$${PROXY_DOMAIN}' < /etc/nginx/supabase-nginx.conf.tpl > /etc/nginx/user_conf.d/nginx.conf && \
|
||||
/scripts/start_nginx_certbot.sh
|
||||
volumes:
|
||||
- ./volumes/proxy/nginx/supabase-nginx.conf.tpl:/etc/nginx/supabase-nginx.conf.tpl:ro
|
||||
- nginx_letsencrypt:/etc/letsencrypt
|
||||
|
||||
volumes:
|
||||
nginx_letsencrypt:
|
||||
@@ -0,0 +1,18 @@
|
||||
# Postgres 15 override for self-hosted Supabase.
|
||||
#
|
||||
# Postgres 17 is now the default in docker-compose.yml. Use this override to pin
|
||||
# Postgres 15, for two cases:
|
||||
# 1. An existing Postgres 15 deployment that has not been upgraded yet.
|
||||
# 2. As the rollback target for utils/upgrade-pg17.sh (PG 15 and PG 17 use
|
||||
# different postgres UIDs, so rollback must start with the PG 15 image).
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.pg15.yml up -d
|
||||
#
|
||||
# To upgrade an existing Postgres 15 database to Postgres 17 in place, see:
|
||||
# - https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17
|
||||
# - utils/upgrade-pg17.sh
|
||||
|
||||
services:
|
||||
db:
|
||||
image: supabase/postgres:15.8.1.085
|
||||
@@ -0,0 +1,19 @@
|
||||
# Postgres 17 override for self-hosted Supabase.
|
||||
#
|
||||
# NOTE: Postgres 17 is now the default in docker-compose.yml, so this override
|
||||
# is redundant for new installs (kept for explicitness / backwards compatibility).
|
||||
# Keep the image tag here in sync with the db image in docker-compose.yml.
|
||||
#
|
||||
# Usage (new install or after upgrade):
|
||||
# docker compose -f docker-compose.yml -f docker-compose.pg17.yml up -d
|
||||
#
|
||||
# For upgrading an existing Postgres 15 database, run utils/upgrade-pg17.sh first.
|
||||
#
|
||||
# Note: PG 15 and PG 17 use different postgres UIDs. If starting fresh with a
|
||||
# leftover db-config volume from PG 15, you may see
|
||||
# "FATAL: invalid secret key". Either remove the old volume or fix ownership.
|
||||
# See: docs/guides/self-hosting/postgres-upgrade-17
|
||||
|
||||
services:
|
||||
db:
|
||||
image: supabase/postgres:17.6.1.136
|
||||
@@ -0,0 +1,56 @@
|
||||
# PgBouncer override for self-hosted Supabase
|
||||
#
|
||||
# Replaces the default Supavisor pooler with PgBouncer in transaction mode.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.pgbouncer.yml up -d
|
||||
#
|
||||
# Or use run.sh to add PgBouncer to the stack:
|
||||
# sh run.sh config add pgbouncer
|
||||
# sh run.sh start
|
||||
#
|
||||
|
||||
services:
|
||||
supavisor: !reset null
|
||||
|
||||
# To expose Postgres directly on POSTGRES_PORT (5432), uncomment the "db"
|
||||
# section below.
|
||||
# WARNING: this opens Postgres to the external traffic. Restrict access at the
|
||||
# network level if necessary.
|
||||
#db:
|
||||
# ports:
|
||||
# - ${POSTGRES_PORT}:${POSTGRES_PORT}
|
||||
|
||||
pgbouncer:
|
||||
container_name: supabase-pgbouncer
|
||||
image: edoburu/pgbouncer:v1.25.2-p0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${POOLER_PROXY_PORT_TRANSACTION}:6432
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-h', 'localhost', '-p', '6432']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# PgBouncer connects to Postgres as the dedicated `pgbouncer` role and
|
||||
# looks up every other role's password on demand via the auth_query below.
|
||||
DB_USER: pgbouncer
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_HOST: ${POSTGRES_HOST}
|
||||
DB_PORT: ${POSTGRES_PORT}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
LISTEN_PORT: 6432
|
||||
AUTH_TYPE: scram-sha-256
|
||||
AUTH_QUERY: SELECT * FROM pgbouncer.get_auth($$1)
|
||||
# POOL_MODE can be: session, transaction, or statement
|
||||
POOL_MODE: transaction
|
||||
DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE}
|
||||
MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN}
|
||||
# Only the `pgbouncer` role may run get_auth / the admin console.
|
||||
ADMIN_USERS: pgbouncer
|
||||
STATS_USERS: pgbouncer
|
||||
@@ -0,0 +1,34 @@
|
||||
# Override di produzione, pensato per girare anche su hardware limitato (es. Raspberry Pi 4).
|
||||
#
|
||||
# 1. Nessuna porta esposta pubblicamente: l'API gateway (Kong/envoy, che serve anche Studio)
|
||||
# resta raggiungibile solo su 127.0.0.1 — mettici davanti un reverse proxy TLS (vedi
|
||||
# Caddyfile.example). Postgres di suo non espone porte nel compose base (supavisor sì,
|
||||
# ma non ci serve: l'app si connette al DB sulla rete interna Docker, non da host esterno).
|
||||
# 2. Servizi mai usati da CrAPP disattivati (nessuna route usa Storage/imgproxy/Realtime/Edge
|
||||
# Functions/pooler — verificato: zero `.storage.`, `.functions.invoke`, `.channel()` nel
|
||||
# codice, e gli avatar sono salvati come dataURL nel DB, non su Storage).
|
||||
# 3. `studio` + `meta` restano attivi: servono per guardare/gestire il database via interfaccia
|
||||
# grafica (raggiungibile tramite lo stesso api-gw su 127.0.0.1:8000, con le credenziali
|
||||
# DASHBOARD_USERNAME/DASHBOARD_PASSWORD del tuo .env).
|
||||
#
|
||||
# Uso: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
|
||||
services:
|
||||
api-gw:
|
||||
# Compose unisce le liste `ports` tra file invece di sostituirle: serve !override
|
||||
# per rimpiazzare davvero il binding pubblico del file base con uno solo-locale.
|
||||
ports: !override
|
||||
- "127.0.0.1:${API_GW_HTTP_PORT:-${KONG_HTTP_PORT:-8000}}:8000/tcp"
|
||||
|
||||
# Mai usati da CrAPP: profilo "unused" mai attivato, quindi `docker compose up -d`
|
||||
# di default non li avvia nemmeno.
|
||||
realtime:
|
||||
profiles: ["unused"]
|
||||
storage:
|
||||
profiles: ["unused"]
|
||||
imgproxy:
|
||||
profiles: ["unused"]
|
||||
functions:
|
||||
profiles: ["unused"]
|
||||
supavisor:
|
||||
profiles: ["unused"]
|
||||
@@ -0,0 +1,47 @@
|
||||
services:
|
||||
|
||||
rustfs:
|
||||
image: rustfs/rustfs:1.0.0-beta.11
|
||||
environment:
|
||||
RUSTFS_ACCESS_KEY: ${MINIO_ROOT_USER}
|
||||
RUSTFS_SECRET_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
RUSTFS_CONSOLE_ADDRESS: 0.0.0.0:9001
|
||||
RUSTFS_CORS_ALLOWED_ORIGINS: "*"
|
||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS: "*"
|
||||
security_opt:
|
||||
- "no-new-privileges:true"
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-f", "http://rustfs:9000/health" ]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- rustfs-data:/data
|
||||
|
||||
rustfs-createbucket:
|
||||
image: rustfs/rc
|
||||
depends_on:
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -ec "
|
||||
rc alias set supa-rustfs http://rustfs:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} &&
|
||||
rc mb --ignore-existing supa-rustfs/${GLOBAL_S3_BUCKET}
|
||||
"
|
||||
|
||||
storage:
|
||||
depends_on:
|
||||
rustfs-createbucket:
|
||||
condition: service_completed_successfully
|
||||
rustfs:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
STORAGE_BACKEND: s3
|
||||
GLOBAL_S3_ENDPOINT: http://rustfs:9000
|
||||
GLOBAL_S3_PROTOCOL: http
|
||||
GLOBAL_S3_FORCE_PATH_STYLE: true
|
||||
AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
||||
AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
|
||||
volumes:
|
||||
rustfs-data:
|
||||
@@ -0,0 +1,46 @@
|
||||
services:
|
||||
|
||||
minio:
|
||||
image: cgr.dev/chainguard/minio
|
||||
#ports:
|
||||
# - '9000:9000'
|
||||
# - '9001:9001'
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
command: server --console-address ":9001" /data
|
||||
healthcheck:
|
||||
test: [ "CMD", "mc", "ready", "local" ]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
|
||||
minio-createbucket:
|
||||
image: cgr.dev/chainguard/minio-client:latest-dev
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -ec "
|
||||
mc alias set supa-minio http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD} &&
|
||||
mc mb --ignore-existing supa-minio/${GLOBAL_S3_BUCKET}
|
||||
"
|
||||
|
||||
storage:
|
||||
depends_on:
|
||||
minio-createbucket:
|
||||
condition: service_completed_successfully
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
STORAGE_BACKEND: s3
|
||||
GLOBAL_S3_ENDPOINT: http://minio:9000
|
||||
GLOBAL_S3_PROTOCOL: http
|
||||
GLOBAL_S3_FORCE_PATH_STYLE: true
|
||||
AWS_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
||||
AWS_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
||||
|
||||
volumes:
|
||||
minio-data:
|
||||
@@ -0,0 +1,587 @@
|
||||
# Usage
|
||||
# Start: docker compose up -d
|
||||
# Stop: docker compose down
|
||||
# Dev mode: docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml up -d
|
||||
# Reset everything: sh reset.sh
|
||||
#
|
||||
# Notes:
|
||||
# - Nested variable interpolation (${A:-${B}}) requires podman-compose >= 1.6.0
|
||||
#
|
||||
|
||||
name: supabase
|
||||
|
||||
services:
|
||||
|
||||
studio:
|
||||
container_name: supabase-studio
|
||||
image: supabase/studio:2026.08.03-sha-022b374
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"node -e \"fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})\""
|
||||
]
|
||||
timeout: 10s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
environment:
|
||||
# Listen on all IPv4 interfaces
|
||||
HOSTNAME: "0.0.0.0"
|
||||
|
||||
STUDIO_PG_META_URL: http://meta:8080
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
# See: https://supabase.com/docs/guides/self-hosting/remove-superuser-access
|
||||
POSTGRES_USER_READ_WRITE: postgres
|
||||
|
||||
PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
|
||||
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||
PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000}
|
||||
PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public}
|
||||
|
||||
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
|
||||
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||
|
||||
SUPABASE_URL: http://api-gw:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY}
|
||||
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY}
|
||||
|
||||
# See: docker-compose.logs.yml
|
||||
ENABLED_FEATURES_LOGS_ALL: "false"
|
||||
|
||||
SNIPPETS_MANAGEMENT_FOLDER: /app/snippets
|
||||
EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions
|
||||
volumes:
|
||||
- ./volumes/snippets:/app/snippets:z
|
||||
- ./volumes/functions:/app/edge-functions:ro,z
|
||||
|
||||
# Envoy is the default API gateway
|
||||
# See: https://github.com/orgs/supabase/discussions/48048
|
||||
api-gw:
|
||||
container_name: supabase-envoy
|
||||
image: envoyproxy/envoy:v1.39.0
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
default:
|
||||
# Expose `envoy` and `kong` as network aliases, so internal configs
|
||||
# that reference either hostname resolve to whichever gateway is active.
|
||||
aliases:
|
||||
- envoy
|
||||
- kong
|
||||
healthcheck:
|
||||
# Using a TCP port check because this image does not include curl or wget.
|
||||
test: ["CMD-SHELL", "timeout 1 bash -c '</dev/tcp/127.0.0.1/8000'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
studio:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- ${API_GW_HTTP_PORT:-${KONG_HTTP_PORT:-8000}}:8000/tcp
|
||||
volumes:
|
||||
- ./volumes/api/envoy/envoy.yaml:/etc/envoy/envoy.yaml:ro
|
||||
- ./volumes/api/envoy/cds.yaml:/etc/envoy/cds.yaml:ro
|
||||
- ./volumes/api/envoy/lds.template.yaml:/etc/envoy/lds.template.yaml:ro
|
||||
- ./volumes/api/envoy/docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
environment:
|
||||
ANON_KEY: ${ANON_KEY}
|
||||
SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-}
|
||||
SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-}
|
||||
ANON_KEY_ASYMMETRIC: ${ANON_KEY_ASYMMETRIC:-}
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC: ${SERVICE_ROLE_KEY_ASYMMETRIC:-}
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
|
||||
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
|
||||
entrypoint: ["/bin/sh", "/docker-entrypoint.sh"]
|
||||
|
||||
auth:
|
||||
container_name: supabase-auth
|
||||
image: supabase/gotrue:v2.189.0
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:9999/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
environment:
|
||||
GOTRUE_API_HOST: 0.0.0.0
|
||||
GOTRUE_API_PORT: 9999
|
||||
API_EXTERNAL_URL: ${API_EXTERNAL_URL}
|
||||
|
||||
GOTRUE_DB_DRIVER: postgres
|
||||
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
|
||||
GOTRUE_SITE_URL: ${SITE_URL}
|
||||
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
|
||||
GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP}
|
||||
|
||||
GOTRUE_JWT_ADMIN_ROLES: service_role
|
||||
GOTRUE_JWT_AUD: authenticated
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JSON array of signing JWKs (EC private + legacy symmetric)
|
||||
# For Podman, use: GOTRUE_JWT_KEYS: ${JWT_KEYS}
|
||||
#GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]}
|
||||
|
||||
GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL}
|
||||
|
||||
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
|
||||
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS}
|
||||
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
|
||||
|
||||
# Uncomment to bypass nonce check in ID Token flow. Commonly set to true when using Google Sign In on mobile.
|
||||
# GOTRUE_EXTERNAL_SKIP_NONCE_CHECK: "true"
|
||||
|
||||
# GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: "true"
|
||||
# GOTRUE_SMTP_MAX_FREQUENCY: 1s
|
||||
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL}
|
||||
GOTRUE_SMTP_HOST: ${SMTP_HOST}
|
||||
GOTRUE_SMTP_PORT: ${SMTP_PORT}
|
||||
GOTRUE_SMTP_USER: ${SMTP_USER}
|
||||
GOTRUE_SMTP_PASS: ${SMTP_PASS}
|
||||
GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME}
|
||||
GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE}
|
||||
GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION}
|
||||
GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY}
|
||||
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE}
|
||||
|
||||
GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP}
|
||||
GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM}
|
||||
|
||||
# Uncomment to enable OAuth / social login providers.
|
||||
# GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOOGLE_ENABLED}
|
||||
# GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
|
||||
# GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOOGLE_SECRET}
|
||||
# GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${API_EXTERNAL_URL}/callback
|
||||
|
||||
# GOTRUE_EXTERNAL_GITHUB_ENABLED: ${GITHUB_ENABLED}
|
||||
# GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
|
||||
# GOTRUE_EXTERNAL_GITHUB_SECRET: ${GITHUB_SECRET}
|
||||
# GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI: ${API_EXTERNAL_URL}/callback
|
||||
|
||||
# GOTRUE_EXTERNAL_AZURE_ENABLED: ${AZURE_ENABLED}
|
||||
# GOTRUE_EXTERNAL_AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
|
||||
# GOTRUE_EXTERNAL_AZURE_SECRET: ${AZURE_SECRET}
|
||||
# GOTRUE_EXTERNAL_AZURE_REDIRECT_URI: ${API_EXTERNAL_URL}/callback
|
||||
|
||||
# Uncomment to configure SMS delivery (phone auth and phone MFA).
|
||||
# GOTRUE_SMS_PROVIDER: ${SMS_PROVIDER}
|
||||
# GOTRUE_SMS_OTP_EXP: ${SMS_OTP_EXP}
|
||||
# GOTRUE_SMS_OTP_LENGTH: ${SMS_OTP_LENGTH}
|
||||
# GOTRUE_SMS_MAX_FREQUENCY: ${SMS_MAX_FREQUENCY}
|
||||
# GOTRUE_SMS_TEMPLATE: ${SMS_TEMPLATE}
|
||||
|
||||
# Twilio credentials (when SMS_PROVIDER=twilio)
|
||||
# GOTRUE_SMS_TWILIO_ACCOUNT_SID: ${SMS_TWILIO_ACCOUNT_SID}
|
||||
# GOTRUE_SMS_TWILIO_AUTH_TOKEN: ${SMS_TWILIO_AUTH_TOKEN}
|
||||
# GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID: ${SMS_TWILIO_MESSAGE_SERVICE_SID}
|
||||
|
||||
# Test OTP mappings for development
|
||||
# GOTRUE_SMS_TEST_OTP: ${SMS_TEST_OTP}
|
||||
|
||||
# Uncomment to configure multi-factor authentication (MFA).
|
||||
# GOTRUE_MFA_TOTP_ENROLL_ENABLED: ${MFA_TOTP_ENROLL_ENABLED}
|
||||
# GOTRUE_MFA_TOTP_VERIFY_ENABLED: ${MFA_TOTP_VERIFY_ENABLED}
|
||||
# GOTRUE_MFA_PHONE_ENROLL_ENABLED: ${MFA_PHONE_ENROLL_ENABLED}
|
||||
# GOTRUE_MFA_PHONE_VERIFY_ENABLED: ${MFA_PHONE_VERIFY_ENABLED}
|
||||
# GOTRUE_MFA_MAX_ENROLLED_FACTORS: ${MFA_MAX_ENROLLED_FACTORS}
|
||||
|
||||
# SAML SSO
|
||||
# See: https://supabase.com/docs/guides/self-hosting/self-hosted-saml-sso
|
||||
# GOTRUE_SAML_ENABLED: ${SAML_ENABLED}
|
||||
# GOTRUE_SAML_PRIVATE_KEY: ${SAML_PRIVATE_KEY}
|
||||
# GOTRUE_SAML_ALLOW_ENCRYPTED_ASSERTIONS: ${SAML_ALLOW_ENCRYPTED_ASSERTIONS}
|
||||
# GOTRUE_SAML_RELAY_STATE_VALIDITY_PERIOD: ${SAML_RELAY_STATE_VALIDITY_PERIOD}
|
||||
# GOTRUE_SAML_RATE_LIMIT_ASSERTION: ${SAML_RATE_LIMIT_ASSERTION}
|
||||
# Optional, defaults to API_EXTERNAL_URL if not set
|
||||
# GOTRUE_SAML_EXTERNAL_URL: ${SAML_EXTERNAL_URL}
|
||||
|
||||
# Uncomment to enable custom access token hook.
|
||||
# See: https://supabase.com/docs/guides/auth/auth-hooks for
|
||||
# full list of hooks and additional details about custom_access_token_hook
|
||||
|
||||
# GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: "true"
|
||||
# GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI: "pg-functions://postgres/public/custom_access_token_hook"
|
||||
# GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: "<standard-base64-secret>"
|
||||
|
||||
# GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED: "true"
|
||||
# GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/mfa_verification_attempt"
|
||||
|
||||
# GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED: "true"
|
||||
# GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: "pg-functions://postgres/public/password_verification_attempt"
|
||||
|
||||
# GOTRUE_HOOK_SEND_SMS_ENABLED: "false"
|
||||
# GOTRUE_HOOK_SEND_SMS_URI: "pg-functions://postgres/public/custom_access_token_hook"
|
||||
# GOTRUE_HOOK_SEND_SMS_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n"
|
||||
|
||||
# GOTRUE_HOOK_SEND_EMAIL_ENABLED: "false"
|
||||
# GOTRUE_HOOK_SEND_EMAIL_URI: "http://host.docker.internal:54321/functions/v1/email_sender"
|
||||
# GOTRUE_HOOK_SEND_EMAIL_SECRETS: "v1,whsec_VGhpcyBpcyBhbiBleGFtcGxlIG9mIGEgc2hvcnRlciBCYXNlNjQgc3RyaW5n"
|
||||
|
||||
rest:
|
||||
container_name: supabase-rest
|
||||
image: postgrest/postgrest:v14.12
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: [ "CMD", "postgrest", "--ready" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||
PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000}
|
||||
PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public}
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
|
||||
PGRST_ADMIN_SERVER_PORT: 3001
|
||||
PGRST_ADMIN_SERVER_HOST: localhost
|
||||
|
||||
# PostgREST accepts a plain-text symmetric secret, a single JWK, or a JWKS.
|
||||
# For Podman, use either PGRST_JWT_SECRET: ${JWT_SECRET} or
|
||||
# PGRST_JWT_SECRET: ${JWT_JWKS}
|
||||
PGRST_JWT_SECRET: ${JWT_JWKS:-${JWT_SECRET}}
|
||||
|
||||
PGRST_DB_USE_LEGACY_GUCS: "false"
|
||||
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
|
||||
PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY}
|
||||
command:
|
||||
[
|
||||
"postgrest"
|
||||
]
|
||||
|
||||
realtime:
|
||||
# This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain
|
||||
container_name: realtime-dev.supabase-realtime
|
||||
image: supabase/realtime:v2.102.3
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"curl -sSfL --head -o /dev/null -H \"Authorization: Bearer ${ANON_KEY}\" http://localhost:4000/api/tenants/realtime-dev/health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 30s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
environment:
|
||||
PORT: 4000
|
||||
DB_HOST: ${POSTGRES_HOST}
|
||||
DB_PORT: ${POSTGRES_PORT}
|
||||
DB_USER: supabase_admin
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_AFTER_CONNECT_QUERY: 'SET search_path TO _realtime'
|
||||
DB_ENC_KEY: ${REALTIME_DB_ENC_KEY:-supabaserealtime}
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# For Podman, use: API_JWT_JWKS: ${JWT_JWKS}
|
||||
#API_JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
|
||||
METRICS_JWT_SECRET: ${JWT_SECRET}
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
DNS_NODES: "''"
|
||||
RLIMIT_NOFILE: "10000"
|
||||
APP_NAME: realtime
|
||||
SEED_SELF_HOST: "true"
|
||||
RUN_JANITOR: "true"
|
||||
DISABLE_HEALTHCHECK_LOGGING: "true"
|
||||
|
||||
# To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up
|
||||
storage:
|
||||
container_name: supabase-storage
|
||||
image: supabase/storage-api:v1.60.4
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
imgproxy:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://storage:5000/status"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
environment:
|
||||
ANON_KEY: ${ANON_KEY}
|
||||
SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
POSTGREST_URL: http://rest:3000
|
||||
|
||||
# Legacy symmetric HS256 key
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# For Podman, use: JWT_JWKS: ${JWT_JWKS}
|
||||
#JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
STORAGE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
REQUEST_ALLOW_X_FORWARDED_PATH: "true"
|
||||
FILE_SIZE_LIMIT: 52428800
|
||||
STORAGE_BACKEND: file
|
||||
# S3 bucket when using S3 backend, directory name when using 'file'
|
||||
GLOBAL_S3_BUCKET: ${GLOBAL_S3_BUCKET}
|
||||
# S3 Backend configuration
|
||||
#GLOBAL_S3_ENDPOINT: https://your-s3-endpoint
|
||||
#GLOBAL_S3_PROTOCOL: https
|
||||
#GLOBAL_S3_FORCE_PATH_STYLE: "true"
|
||||
#AWS_ACCESS_KEY_ID: your-access-key-id
|
||||
#AWS_SECRET_ACCESS_KEY: your-secret-access-key
|
||||
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
|
||||
TENANT_ID: ${STORAGE_TENANT_ID}
|
||||
# TODO: https://github.com/supabase/storage-api/issues/55
|
||||
REGION: ${REGION}
|
||||
ENABLE_IMAGE_TRANSFORMATION: "true"
|
||||
IMGPROXY_URL: http://imgproxy:5001
|
||||
# S3 protocol endpoint configuration
|
||||
S3_PROTOCOL_ACCESS_KEY_ID: ${S3_PROTOCOL_ACCESS_KEY_ID}
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET: ${S3_PROTOCOL_ACCESS_KEY_SECRET}
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:z
|
||||
|
||||
imgproxy:
|
||||
container_name: supabase-imgproxy
|
||||
image: darthsim/imgproxy:v3.30.1
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/storage:/var/lib/storage:z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"imgproxy",
|
||||
"health"
|
||||
]
|
||||
timeout: 5s
|
||||
interval: 5s
|
||||
retries: 3
|
||||
environment:
|
||||
IMGPROXY_BIND: ":5001"
|
||||
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
|
||||
IMGPROXY_USE_ETAG: "true"
|
||||
IMGPROXY_AUTO_WEBP: ${IMGPROXY_AUTO_WEBP}
|
||||
IMGPROXY_MAX_SRC_RESOLUTION: 16.8
|
||||
|
||||
meta:
|
||||
container_name: supabase-meta
|
||||
image: supabase/postgres-meta:v0.96.6
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
# Disable this if you are using an external Postgres database
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PG_META_PORT: 8080
|
||||
PG_META_DB_HOST: ${POSTGRES_HOST}
|
||||
PG_META_DB_PORT: ${POSTGRES_PORT}
|
||||
PG_META_DB_NAME: ${POSTGRES_DB}
|
||||
PG_META_DB_USER: postgres
|
||||
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
CRYPTO_KEY: ${PG_META_CRYPTO_KEY}
|
||||
|
||||
functions:
|
||||
container_name: supabase-edge-functions
|
||||
image: supabase/edge-runtime:v1.74.0
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/functions:/home/deno/functions:z
|
||||
- deno-cache:/root/.cache/deno
|
||||
depends_on:
|
||||
api-gw:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "timeout 1 bash -c '</dev/tcp/127.0.0.1/9000'"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
environment:
|
||||
# Legacy symmetric HS256 key
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
# JWKS for token verification (EC public + legacy symmetric).
|
||||
# For Podman, use: SUPABASE_JWKS: ${JWT_JWKS}
|
||||
#SUPABASE_JWKS: ${JWT_JWKS:-{"keys":[]}}
|
||||
|
||||
SUPABASE_URL: http://api-gw:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
# Legacy API keys (HS256-signed JWTs)
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
# New opaque API keys
|
||||
SUPABASE_PUBLISHABLE_KEYS: "{\"default\":\"${SUPABASE_PUBLISHABLE_KEY:-}\"}"
|
||||
SUPABASE_SECRET_KEYS: "{\"default\":\"${SUPABASE_SECRET_KEY:-}\"}"
|
||||
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
# TODO: Allow configuring VERIFY_JWT per function.
|
||||
VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}"
|
||||
command:
|
||||
[
|
||||
"start",
|
||||
"--main-service",
|
||||
"/home/deno/functions/main"
|
||||
]
|
||||
|
||||
# Comment out everything below this point if you are using an external Postgres database
|
||||
db:
|
||||
container_name: supabase-db
|
||||
# To upgrade an existing Postgres 15 database in place, see utils/upgrade-pg17.sh.
|
||||
image: supabase/postgres:17.6.1.136
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z
|
||||
# Must be superuser to create event trigger
|
||||
- ./volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z
|
||||
# Must be superuser to alter reserved role
|
||||
- ./volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z
|
||||
# Initialize the database settings with JWT_SECRET and JWT_EXP
|
||||
- ./volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z
|
||||
# PGDATA directory is persisted between restarts
|
||||
- ./volumes/db/data:/var/lib/postgresql/data:Z
|
||||
# Changes required for internal supabase data such as _analytics
|
||||
- ./volumes/db/_supabase.sql:/docker-entrypoint-initdb.d/migrations/97-_supabase.sql:Z
|
||||
# Changes required for Analytics support
|
||||
- ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z
|
||||
# Changes required for Pooler support
|
||||
- ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/99-pooler.sql:Z
|
||||
# Use named volume to persist pgsodium decryption key between restarts
|
||||
- db-config:/etc/postgresql-custom
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"pg_isready",
|
||||
"-U",
|
||||
"postgres",
|
||||
"-h",
|
||||
"localhost"
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
environment:
|
||||
POSTGRES_HOST: /var/run/postgresql
|
||||
PGPORT: ${POSTGRES_PORT}
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
PGPASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
PGDATABASE: ${POSTGRES_DB}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_EXP: ${JWT_EXPIRY}
|
||||
command:
|
||||
[
|
||||
"postgres",
|
||||
"-c",
|
||||
"config_file=/etc/postgresql/postgresql.conf",
|
||||
"-c",
|
||||
"log_min_messages=fatal" # prevents Realtime polling queries from appearing in logs
|
||||
]
|
||||
|
||||
# Update the DATABASE_URL if you are using an external Postgres database
|
||||
supavisor:
|
||||
container_name: supabase-pooler
|
||||
image: supabase/supavisor:2.9.5
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- ${POSTGRES_PORT}:5432
|
||||
- ${POOLER_PROXY_PORT_TRANSACTION}:6543
|
||||
volumes:
|
||||
- ./volumes/pooler/pooler.exs:/etc/pooler/pooler.exs:ro,z
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"curl",
|
||||
"-sSfL",
|
||||
"--head",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
"http://127.0.0.1:4000/api/health"
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PORT: 4000
|
||||
POSTGRES_PORT: ${POSTGRES_PORT}
|
||||
POSTGRES_HOST: ${POSTGRES_HOST}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DATABASE_URL: ecto://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase
|
||||
CLUSTER_POSTGRES: "true"
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
|
||||
VAULT_ENC_KEY: ${VAULT_ENC_KEY}
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
METRICS_JWT_SECRET: ${JWT_SECRET}
|
||||
REGION: local
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
POOLER_TENANT_ID: ${POOLER_TENANT_ID}
|
||||
POOLER_DEFAULT_POOL_SIZE: ${POOLER_DEFAULT_POOL_SIZE}
|
||||
POOLER_MAX_CLIENT_CONN: ${POOLER_MAX_CLIENT_CONN}
|
||||
POOLER_POOL_MODE: transaction
|
||||
DB_POOL_SIZE: ${POOLER_DB_POOL_SIZE}
|
||||
command:
|
||||
[
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"/app/bin/migrate && /app/bin/supavisor eval \"$$(cat /etc/pooler/pooler.exs)\" && /app/bin/server"
|
||||
]
|
||||
|
||||
volumes:
|
||||
db-config:
|
||||
deno-cache:
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
auto_confirm=0
|
||||
|
||||
confirm () {
|
||||
if [ "$auto_confirm" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "Are you sure you want to proceed? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy])
|
||||
;;
|
||||
*)
|
||||
echo "Script canceled."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "$1" = "-y" ]; then
|
||||
auto_confirm=1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "*** WARNING: This will remove all containers and container data, and optionally reset .env ***"
|
||||
echo ""
|
||||
|
||||
confirm
|
||||
|
||||
echo "===> Stopping and removing all containers..."
|
||||
|
||||
if [ -f ".env" ]; then
|
||||
docker compose -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
|
||||
elif [ -f ".env.example" ]; then
|
||||
echo "No .env found, using .env.example for docker compose down..."
|
||||
docker compose --env-file .env.example -f docker-compose.yml -f ./dev/docker-compose.dev.yml down -v --remove-orphans
|
||||
else
|
||||
echo "Skipping 'docker compose down' because there's no env-file."
|
||||
fi
|
||||
|
||||
echo "===> Cleaning up bind-mounted directories..."
|
||||
BIND_MOUNTS="./volumes/db/data ./volumes/storage"
|
||||
|
||||
for dir in $BIND_MOUNTS; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "Removing $dir..."
|
||||
confirm
|
||||
rm -rf "$dir"
|
||||
else
|
||||
echo "$dir not found."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "===> Resetting .env file (will save backup to .env.old)..."
|
||||
confirm
|
||||
if [ -f ".env" ] || [ -L ".env" ]; then
|
||||
echo "Renaming existing .env file to .env.old"
|
||||
mv .env .env.old
|
||||
else
|
||||
echo "No .env file found."
|
||||
fi
|
||||
|
||||
if [ -f ".env.example" ]; then
|
||||
echo "===> Copying .env.example to .env"
|
||||
cp .env.example .env
|
||||
else
|
||||
echo "No .env.example found, can't restore .env to default values."
|
||||
fi
|
||||
|
||||
echo "Cleanup complete!"
|
||||
echo "Re-run 'docker compose pull' to update images."
|
||||
echo ""
|
||||
Executable
+297
@@ -0,0 +1,297 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Manage the self-hosted Supabase docker compose stack.
|
||||
#
|
||||
# Override files are layered via docker compose's native COMPOSE_FILE env
|
||||
# var in .env. Format: colon-separated list with docker-compose.yml first.
|
||||
#
|
||||
# Examples in .env:
|
||||
# COMPOSE_FILE=docker-compose.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
|
||||
#
|
||||
# Manage with: sh run.sh config add <name> | config remove <name>
|
||||
# (accepts either a short name like 'pg17' or 'docker-compose.pg17.yml')
|
||||
#
|
||||
# Usage:
|
||||
# sh run.sh start # docker compose up -d --wait
|
||||
# sh run.sh stop # docker compose down
|
||||
# sh run.sh restart [service] # restart the stack (or named services)
|
||||
# sh run.sh restart --except <svc>... # restart all services except the named ones
|
||||
# sh run.sh recreate [service] # stop then start (or force-recreate one service)
|
||||
# sh run.sh recreate --except <svc>... # force-recreate all services except the named ones
|
||||
# sh run.sh status # docker compose ps
|
||||
# sh run.sh logs [service] # follow logs (all or one service)
|
||||
# sh run.sh inspect <service> # docker inspect on a service's container
|
||||
# sh run.sh printenv <service> # print a service's environment variables
|
||||
# sh run.sh pull # pull images
|
||||
# sh run.sh config # show the active COMPOSE_FILE list
|
||||
# sh run.sh config add <name> # add an override to COMPOSE_FILE in .env
|
||||
# sh run.sh config remove <name> # remove an override from COMPOSE_FILE in .env
|
||||
# sh run.sh compose-config # dump fully-resolved docker compose config
|
||||
# sh run.sh secrets # print key passwords and API keys from .env
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
echo "ERROR: docker-compose.yml not found in $(pwd)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Normalize an override argument:
|
||||
# pg17 -> docker-compose.pg17.yml
|
||||
# docker-compose.pg17.yml -> docker-compose.pg17.yml
|
||||
# ./docker-compose.pg17.yml -> docker-compose.pg17.yml
|
||||
# docker-compose.yml -> error (base file, always implicit)
|
||||
normalize_override() {
|
||||
arg="${1#./}"
|
||||
case "$arg" in
|
||||
docker-compose.yml)
|
||||
echo "ERROR: docker-compose.yml is the base file, always included" >&2
|
||||
return 1
|
||||
;;
|
||||
docker-compose.*.yml)
|
||||
echo "$arg"
|
||||
;;
|
||||
*)
|
||||
echo "docker-compose.${arg}.yml"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Read COMPOSE_FILE from .env (stripping quotes and CR).
|
||||
read_compose_file() {
|
||||
[ -f .env ] || return 0
|
||||
grep '^COMPOSE_FILE=' .env | head -n1 | cut -d= -f2- | tr -d "\r\"'"
|
||||
}
|
||||
|
||||
# Pretty-print the effective compose file list.
|
||||
print_config() {
|
||||
val="$1"
|
||||
[ -z "$val" ] && val="docker-compose.yml"
|
||||
echo "COMPOSE_FILE=$val"
|
||||
echo "compose files:"
|
||||
OLD_IFS=$IFS
|
||||
IFS=:
|
||||
for f in $val; do
|
||||
echo " $f"
|
||||
done
|
||||
IFS=$OLD_IFS
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Update or append COMPOSE_FILE in .env.
|
||||
write_compose_file() {
|
||||
new_value="$1"
|
||||
if [ ! -f .env ]; then
|
||||
echo "ERROR: .env not found in $(pwd)" >&2
|
||||
exit 1
|
||||
fi
|
||||
new_line="COMPOSE_FILE=$new_value"
|
||||
if grep -q '^COMPOSE_FILE=' .env; then
|
||||
sed -i.old -e "s|^COMPOSE_FILE=.*$|$new_line|" .env
|
||||
rm -f .env.old
|
||||
else
|
||||
cat >> .env <<EOF
|
||||
|
||||
############
|
||||
# Docker compose override files to layer on top of docker-compose.yml.
|
||||
# Colon-separated list. Manage with: sh run.sh config add|remove <name>.
|
||||
#
|
||||
# Examples:
|
||||
# COMPOSE_FILE=docker-compose.yml
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.pg17.yml
|
||||
############
|
||||
$new_line
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# Echoes the list of services (one per line) minus those passed as args.
|
||||
# Warns on unknown names; returns 1 if no services remain.
|
||||
services_except() {
|
||||
all_services=$(docker compose config --services)
|
||||
filtered="$all_services"
|
||||
for ex in "$@"; do
|
||||
echo "$all_services" | grep -qFx "$ex" \
|
||||
|| echo "Warning: '$ex' is not a service in this project" >&2
|
||||
filtered=$(echo "$filtered" | grep -vFx "$ex" || true)
|
||||
done
|
||||
if [ -z "$filtered" ]; then
|
||||
echo "No services left after applying --except" >&2
|
||||
return 1
|
||||
fi
|
||||
printf '%s\n' "$filtered"
|
||||
}
|
||||
|
||||
CMD="${1:-help}"
|
||||
[ "$#" -gt 0 ] && shift
|
||||
|
||||
case "$CMD" in
|
||||
start|up)
|
||||
exec docker compose up -d --wait "$@"
|
||||
;;
|
||||
stop|down)
|
||||
exec docker compose down "$@"
|
||||
;;
|
||||
restart)
|
||||
if [ "${1:-}" = "--except" ]; then
|
||||
shift
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") restart --except <svc>..." >&2; exit 1; }
|
||||
services=$(services_except "$@") || exit 1
|
||||
# shellcheck disable=SC2086
|
||||
exec docker compose restart $services
|
||||
fi
|
||||
exec docker compose restart "$@"
|
||||
;;
|
||||
recreate)
|
||||
if [ "${1:-}" = "--except" ]; then
|
||||
shift
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") recreate --except <svc>..." >&2; exit 1; }
|
||||
services=$(services_except "$@") || exit 1
|
||||
# shellcheck disable=SC2086
|
||||
exec docker compose up -d --wait --force-recreate --no-deps $services
|
||||
fi
|
||||
if [ $# -eq 0 ]; then
|
||||
docker compose down
|
||||
exec docker compose up -d --wait
|
||||
fi
|
||||
# Single-service recreate: force-recreate the named services only,
|
||||
# leave their dependencies running.
|
||||
exec docker compose up -d --wait --force-recreate --no-deps "$@"
|
||||
;;
|
||||
status|ps)
|
||||
exec docker compose ps "$@"
|
||||
;;
|
||||
logs)
|
||||
exec docker compose logs -f "$@"
|
||||
;;
|
||||
inspect)
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") inspect <service> [docker-inspect-args]" >&2; exit 1; }
|
||||
svc="$1"; shift
|
||||
cid=$(docker compose ps -q "$svc")
|
||||
[ -z "$cid" ] && { echo "Service '$svc' is not running" >&2; exit 1; }
|
||||
exec docker inspect "$cid" "$@"
|
||||
;;
|
||||
printenv)
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") printenv <service>" >&2; exit 1; }
|
||||
svc="$1"
|
||||
cid=$(docker compose ps -q "$svc")
|
||||
[ -z "$cid" ] && { echo "Service '$svc' is not running" >&2; exit 1; }
|
||||
exec docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "$cid"
|
||||
;;
|
||||
pull)
|
||||
exec docker compose pull "$@"
|
||||
;;
|
||||
compose-config)
|
||||
exec docker compose config "$@"
|
||||
;;
|
||||
config)
|
||||
sub="${1:-show}"
|
||||
[ "$#" -gt 0 ] && shift
|
||||
current=$(read_compose_file)
|
||||
case "$sub" in
|
||||
show)
|
||||
print_config "$current"
|
||||
;;
|
||||
add)
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") config add <name>..." >&2; exit 1; }
|
||||
new_value="${current:-docker-compose.yml}"
|
||||
changed=false
|
||||
for arg in "$@"; do
|
||||
file=$(normalize_override "$arg") || exit 1
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "ERROR: $file not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
case ":$new_value:" in
|
||||
*":$file:"*) echo "Already present: $file" ;;
|
||||
*) new_value="$new_value:$file"; changed=true ;;
|
||||
esac
|
||||
done
|
||||
[ "$changed" = true ] && write_compose_file "$new_value"
|
||||
print_config "$new_value"
|
||||
;;
|
||||
remove|rm)
|
||||
[ $# -eq 0 ] && { echo "Usage: $(basename "$0") config remove <name>..." >&2; exit 1; }
|
||||
new_value="${current:-docker-compose.yml}"
|
||||
changed=false
|
||||
for arg in "$@"; do
|
||||
file=$(normalize_override "$arg") || exit 1
|
||||
case ":$new_value:" in
|
||||
*":$file:"*)
|
||||
# Drop $file by rebuilding the colon list
|
||||
tmp=""
|
||||
OLD_IFS=$IFS
|
||||
IFS=:
|
||||
for tok in $new_value; do
|
||||
[ "$tok" = "$file" ] || tmp="${tmp:+$tmp:}$tok"
|
||||
done
|
||||
IFS=$OLD_IFS
|
||||
new_value="$tmp"
|
||||
changed=true
|
||||
;;
|
||||
*) echo "Not present: $file" ;;
|
||||
esac
|
||||
done
|
||||
[ "$changed" = true ] && write_compose_file "$new_value"
|
||||
print_config "$new_value"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown config subcommand: $sub" >&2
|
||||
echo "Use: config | config add <name>... | config remove <name>..." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
secrets)
|
||||
if [ ! -f .env ]; then
|
||||
echo "ERROR: .env not found in $(pwd)" >&2
|
||||
exit 1
|
||||
fi
|
||||
for var in POSTGRES_PASSWORD DASHBOARD_PASSWORD \
|
||||
SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY \
|
||||
S3_PROTOCOL_ACCESS_KEY_ID S3_PROTOCOL_ACCESS_KEY_SECRET; do
|
||||
line=$(grep "^${var}=" .env | head -n1)
|
||||
if [ -n "$line" ]; then
|
||||
echo "$line"
|
||||
else
|
||||
echo "${var}="
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
;;
|
||||
help|-h|--help)
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <command>
|
||||
|
||||
Commands:
|
||||
start Start the stack (docker compose up -d --wait)
|
||||
stop Stop the stack (docker compose down)
|
||||
restart [service] Restart the stack (or named services)
|
||||
restart --except <svc>...
|
||||
Restart all services except the named ones
|
||||
recreate [service] Stop then start, or force-recreate one service (--no-deps)
|
||||
recreate --except <svc>...
|
||||
Force-recreate all services except the named ones (--no-deps)
|
||||
status Show service status
|
||||
logs [service] Follow logs (optionally for a single service)
|
||||
inspect <service> Inspect a service's container (forwards extra args to docker inspect)
|
||||
printenv <service> Print a service's environment variables (one per line)
|
||||
pull Pull all images
|
||||
config Show the active COMPOSE_FILE list
|
||||
config add <name> Add an override to COMPOSE_FILE in .env (short name or full filename)
|
||||
config remove <name> Remove an override from COMPOSE_FILE in .env
|
||||
compose-config Dump the fully-resolved docker compose config
|
||||
secrets Show key passwords and API keys from .env
|
||||
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $CMD" >&2
|
||||
echo "Run '$0 help' for usage." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Executable
+455
@@ -0,0 +1,455 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Bootstrap a self-hosted Supabase project on Linux (Debian/Ubuntu or RHEL/CentOS/Fedora).
|
||||
#
|
||||
# What it does:
|
||||
# 1. Installs prerequisites: git, openssl, jq, ca-certificates
|
||||
# 2. Installs Docker Engine + Compose plugin (if missing)
|
||||
# 3. Optionally installs the AWS CLI v2 (--with-aws)
|
||||
# 4. Sparse-clones the repo to extract the contents of ./docker
|
||||
# 5. Creates a project directory in CWD and copies docker/* into it
|
||||
# 6. Records the base version the deployment was set up from (.supabase-version)
|
||||
# 7. Prompts for the main URLs and writes them to .env
|
||||
# 8. Generates secrets and asymmetric API keys via utils/*.sh
|
||||
#
|
||||
# Usage:
|
||||
# sh setup.sh # interactive
|
||||
# sh setup.sh -y # accept defaults, no prompts
|
||||
# sh setup.sh --project-dir my-supabase # name the project directory
|
||||
# sh setup.sh --skip-deps # skip system-package installation
|
||||
# sh setup.sh --with-aws # also install the AWS CLI v2
|
||||
# sh setup.sh --ref self-hosted/v0.7.0 # clone docker/ from a specific git ref
|
||||
# sh setup.sh --head # clone docker/ from HEAD (skip tags)
|
||||
#
|
||||
# curl -fsSL <url-to-this-script> | sh # bootstrap from scratch in CWD
|
||||
#
|
||||
# By default the docker/ sources are cloned from the latest self-hosted release
|
||||
# tag (self-hosted/v*), falling back to the default branch (HEAD) if none exist.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_DIR="supabase-project"
|
||||
SKIP_DEPS=0
|
||||
WITH_AWS=0
|
||||
ASSUME_YES=0
|
||||
SOURCE_REF=""
|
||||
FORCE_HEAD=0
|
||||
|
||||
print_help() {
|
||||
cat <<EOF
|
||||
Usage: setup.sh [options]
|
||||
|
||||
Options:
|
||||
-p, --project-dir <name> Name of the project directory (default: supabase-project)
|
||||
--skip-deps Skip installation of system packages
|
||||
--with-aws Install the AWS CLI v2
|
||||
--ref <tag|branch> Clone docker/ from this git ref instead of the
|
||||
latest self-hosted tag (no HEAD fallback)
|
||||
--head Clone docker/ from the default branch (HEAD),
|
||||
skipping self-hosted tag detection
|
||||
-y, --yes Non-interactive: accept defaults, no prompts
|
||||
-h, --help Show this help and exit
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-p|--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
||||
--skip-deps) SKIP_DEPS=1; shift ;;
|
||||
--with-aws) WITH_AWS=1; shift ;;
|
||||
--ref) SOURCE_REF="$2"; shift 2 ;;
|
||||
--head) FORCE_HEAD=1; shift ;;
|
||||
-y|--yes) ASSUME_YES=1; shift ;;
|
||||
-h|--help) print_help; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; print_help; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Interactive vs not: -y forces non-interactive; otherwise we're non-interactive
|
||||
# when there's no controlling terminal to prompt on (e.g. curl | sh in CI).
|
||||
if [ "$ASSUME_YES" = "1" ] || ! ( : < /dev/tty ) 2>/dev/null; then
|
||||
NON_INTERACTIVE=1
|
||||
else
|
||||
NON_INTERACTIVE=0
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
SUDO=""
|
||||
else
|
||||
SUDO="sudo"
|
||||
fi
|
||||
|
||||
log() { printf "===> %s\n" "$*"; }
|
||||
warn() { printf "WARNING: %s\n" "$*" >&2; }
|
||||
die() { printf "ERROR: %s\n" "$*" >&2; exit 1; }
|
||||
|
||||
# Prompt with a default; echoes the chosen value on stdout.
|
||||
# Reads from /dev/tty so prompts work even when stdin is a pipe (curl | sh).
|
||||
# Falls back to the default with -y or when no controlling terminal exists.
|
||||
ask() {
|
||||
# ask <prompt> <default> -> echoes chosen value
|
||||
if [ "$NON_INTERACTIVE" = "1" ]; then
|
||||
printf '%s' "$2"
|
||||
return
|
||||
fi
|
||||
printf "%s [%s]: " "$1" "$2" > /dev/tty
|
||||
read -r reply < /dev/tty
|
||||
[ -z "$reply" ] && reply="$2"
|
||||
printf '%s' "$reply"
|
||||
}
|
||||
|
||||
# True if the value looks like an http(s) URL (scheme check only, not validation).
|
||||
valid_url() {
|
||||
case "$1" in
|
||||
http://?*|https://?*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Like ask, but requires an http(s):// value. Re-prompts interactively; with no
|
||||
# usable terminal (-y or curl | sh) a bad value is fatal rather than silently kept.
|
||||
ask_url() {
|
||||
# ask_url <prompt> <default> -> echoes a validated URL
|
||||
while :; do
|
||||
_url=$(ask "$1" "$2")
|
||||
valid_url "$_url" && { printf '%s' "$_url"; return 0; }
|
||||
if [ "$NON_INTERACTIVE" = "1" ]; then
|
||||
die "$1 must start with http:// or https:// (got: '$_url')"
|
||||
fi
|
||||
printf " '%s' is not a URL - it must start with http:// or https://.\n" "$_url" > /dev/tty
|
||||
done
|
||||
}
|
||||
|
||||
OS_FAMILY=""
|
||||
OS_ID=""
|
||||
OS_CODENAME=""
|
||||
|
||||
detect_os() {
|
||||
[ -f /etc/os-release ] || die "Cannot detect OS: /etc/os-release missing. Linux only."
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
OS_ID="$ID"
|
||||
OS_CODENAME="${VERSION_CODENAME:-}"
|
||||
case "$ID" in
|
||||
ubuntu|debian) OS_FAMILY="debian" ;;
|
||||
centos|rhel|fedora|rocky|almalinux|ol|amzn) OS_FAMILY="rhel" ;;
|
||||
*)
|
||||
case "${ID_LIKE:-}" in
|
||||
*debian*|*ubuntu*) OS_FAMILY="debian" ;;
|
||||
*rhel*|*fedora*|*centos*) OS_FAMILY="rhel" ;;
|
||||
*) die "Unsupported distribution: $ID" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
log "Detected OS: $ID ($OS_FAMILY)"
|
||||
}
|
||||
|
||||
pkg_update() {
|
||||
if [ "$OS_FAMILY" = "debian" ]; then
|
||||
$SUDO apt-get update -qq -y
|
||||
else
|
||||
$SUDO dnf makecache -q -y || true
|
||||
fi
|
||||
}
|
||||
|
||||
pkg_install() {
|
||||
if [ "$OS_FAMILY" = "debian" ]; then
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
$SUDO apt-get install -qq -y "$@"
|
||||
else
|
||||
$SUDO dnf install -q -y "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
install_base_packages() {
|
||||
log "Installing base packages: git, openssl, jq, ca-certificates"
|
||||
pkg_update
|
||||
if [ "$OS_FAMILY" = "debian" ]; then
|
||||
pkg_install git openssl jq ca-certificates \
|
||||
apt-transport-https gnupg lsb-release
|
||||
else
|
||||
pkg_install git openssl jq ca-certificates dnf-plugins-core
|
||||
fi
|
||||
}
|
||||
|
||||
docker_present() {
|
||||
command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
if docker_present; then
|
||||
log "Docker already installed: $(docker --version)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Installing Docker Engine and Compose plugin"
|
||||
if [ "$OS_FAMILY" = "debian" ]; then
|
||||
$SUDO install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL "https://download.docker.com/linux/${OS_ID}/gpg" \
|
||||
| $SUDO gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg
|
||||
$SUDO chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
codename="${OS_CODENAME:-$(lsb_release -cs 2>/dev/null || echo stable)}"
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${OS_ID} ${codename} stable" \
|
||||
| $SUDO tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
$SUDO apt-get update -y
|
||||
pkg_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
else
|
||||
# Amazon Linux
|
||||
if [ "$OS_ID" = "amzn" ]; then
|
||||
# Install Docker from the repo
|
||||
pkg_install docker
|
||||
# Install Docker Compose
|
||||
$SUDO mkdir -p /usr/local/lib/docker/cli-plugins && \
|
||||
$SUDO curl -fsSL "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" \
|
||||
-o /usr/local/lib/docker/cli-plugins/docker-compose && \
|
||||
$SUDO chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||
else
|
||||
repo_distro="centos"
|
||||
case "$OS_ID" in
|
||||
fedora) repo_distro="fedora" ;;
|
||||
rhel) repo_distro="rhel" ;;
|
||||
esac
|
||||
$SUDO dnf config-manager --add-repo "https://download.docker.com/linux/${repo_distro}/docker-ce.repo" 2>/dev/null \
|
||||
|| $SUDO dnf-3 config-manager --add-repo "https://download.docker.com/linux/${repo_distro}/docker-ce.repo"
|
||||
pkg_install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Enabling and starting docker service"
|
||||
$SUDO systemctl enable --now docker || warn "Could not enable docker via systemctl; start it manually."
|
||||
|
||||
docker_present || die "Docker installation finished but 'docker compose' is still unavailable."
|
||||
}
|
||||
|
||||
install_aws() {
|
||||
if command -v aws >/dev/null 2>&1; then
|
||||
log "AWS CLI already installed: $(aws --version 2>&1)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
arch=$(uname -m)
|
||||
case "$arch" in
|
||||
x86_64|amd64) aws_arch="x86_64" ;;
|
||||
aarch64|arm64) aws_arch="aarch64" ;;
|
||||
*) die "Unsupported architecture for AWS CLI: $arch" ;;
|
||||
esac
|
||||
|
||||
log "Installing AWS CLI v2 (${aws_arch})"
|
||||
command -v unzip >/dev/null 2>&1 || pkg_install unzip
|
||||
tmp=$(mktemp -d)
|
||||
(
|
||||
cd "$tmp"
|
||||
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${aws_arch}.zip" -o awscliv2.zip
|
||||
unzip -q -o awscliv2.zip
|
||||
$SUDO ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update
|
||||
)
|
||||
rm -rf "$tmp"
|
||||
}
|
||||
|
||||
SRC_DIR=""
|
||||
SRC_TMP=""
|
||||
RESOLVED_REF=""
|
||||
REPO_URL="${SUPABASE_REPO_URL:-https://github.com/supabase/supabase}"
|
||||
STAMP_FILE=".supabase-version"
|
||||
|
||||
# Highest self-hosted/v* tag on the remote, or empty when the remote has none.
|
||||
# Returns non-zero (printing nothing) when the remote can't be reached.
|
||||
latest_release_tag() {
|
||||
_refs=$(git ls-remote --tags --refs "$REPO_URL" 2>/dev/null) || return 1
|
||||
printf '%s\n' "$_refs" \
|
||||
| sed 's#^.*refs/tags/##' \
|
||||
| grep -E '^self-hosted/v[0-9]' \
|
||||
| sort -V | tail -n1
|
||||
}
|
||||
|
||||
# Clone only docker/ from <ref> (empty = default branch) into <dest>.
|
||||
sparse_clone() {
|
||||
# sparse_clone <dest> [ref]
|
||||
_dest="$1"
|
||||
_ref="$2"
|
||||
if [ -n "$_ref" ]; then
|
||||
git clone --filter=blob:none --no-checkout --depth=1 --quiet \
|
||||
--branch "$_ref" "$REPO_URL" "$_dest" 2>/dev/null || return 1
|
||||
else
|
||||
git clone --filter=blob:none --no-checkout --depth=1 --quiet \
|
||||
"$REPO_URL" "$_dest" 2>/dev/null || return 1
|
||||
fi
|
||||
( cd "$_dest" &&
|
||||
git sparse-checkout init --cone &&
|
||||
git sparse-checkout set docker &&
|
||||
git checkout --quiet ) 2>/dev/null || return 1
|
||||
}
|
||||
|
||||
# Echo the commit the clone resolved to (for stamping HEAD-based checkouts).
|
||||
resolved_sha() {
|
||||
git -C "$1" rev-parse HEAD 2>/dev/null || true
|
||||
}
|
||||
|
||||
prepare_source() {
|
||||
SRC_TMP=$(mktemp -d) || die "Could not create a temporary directory"
|
||||
dest="$SRC_TMP/supabase"
|
||||
|
||||
# Pick the ref to clone; empty means the default branch (HEAD), used only
|
||||
# when no release tag exists yet (or --head). A resolved tag that fails to
|
||||
# clone is an error, not a reason to silently install HEAD instead.
|
||||
if [ -n "$SOURCE_REF" ]; then
|
||||
ref="$SOURCE_REF"
|
||||
elif [ "$FORCE_HEAD" = "1" ]; then
|
||||
ref=""
|
||||
else
|
||||
if ref=$(latest_release_tag); then
|
||||
[ -n "$ref" ] || log "No self-hosted release tag found; using the default branch (HEAD)"
|
||||
else
|
||||
die "Could not reach $REPO_URL to look up release tags. Check your network and retry, or pass --head (default branch) or --ref <tag>."
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Sparse-cloning supabase repo at ${ref:-HEAD}"
|
||||
sparse_clone "$dest" "$ref" || die "Could not clone '${ref:-HEAD}' from $REPO_URL"
|
||||
|
||||
# Stamp the tag name for a release tag, else the exact commit SHA.
|
||||
case "$ref" in
|
||||
self-hosted/v*) RESOLVED_REF="$ref" ;;
|
||||
*) RESOLVED_REF=$(resolved_sha "$dest") ;;
|
||||
esac
|
||||
|
||||
SRC_DIR="$dest/docker"
|
||||
}
|
||||
|
||||
cleanup_src_tmp() {
|
||||
if [ -n "$SRC_TMP" ] && [ -d "$SRC_TMP" ]; then
|
||||
rm -rf "$SRC_TMP"
|
||||
fi
|
||||
}
|
||||
trap cleanup_src_tmp EXIT
|
||||
|
||||
read_env() {
|
||||
grep "^$1=" .env 2>/dev/null | head -n1 | cut -d= -f2-
|
||||
}
|
||||
|
||||
# Record the base version this deployment was set up from, so update.sh can
|
||||
# 3-way merge future upgrades against it: it fetches the snapshot at this ref
|
||||
# as the merge base, and derives the base version from the ref itself. Just the
|
||||
# ref - per-deployment state, not vendor content: gitignored.
|
||||
write_version_stamp() {
|
||||
[ -n "$1" ] || { warn "Could not resolve a base ref; skipping $STAMP_FILE"; return 0; }
|
||||
{
|
||||
echo "# Supabase self-hosted version stamp. Managed by setup.sh / update.sh."
|
||||
echo "# Do not commit or edit by hand. Records the ref this deployment was based on."
|
||||
echo "ref=$1"
|
||||
} > "$STAMP_FILE"
|
||||
log "Recorded base version in $STAMP_FILE (ref=$1)"
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
||||
log "Setup starting in $(pwd)"
|
||||
log "This may take several minutes..."
|
||||
|
||||
if [ "$SKIP_DEPS" = "1" ]; then
|
||||
log "Skipping system-package installation (--skip-deps)"
|
||||
else
|
||||
detect_os
|
||||
install_base_packages
|
||||
install_docker
|
||||
fi
|
||||
|
||||
if [ "$WITH_AWS" = "1" ]; then
|
||||
install_aws
|
||||
fi
|
||||
|
||||
# Idempotent re-run: if CWD is already a set-up project, skip bootstrap.
|
||||
# A clone has docker-compose.yml + utils/ but only .env.example;
|
||||
# a set-up project also has a real .env.
|
||||
if [ -f .env ] && [ -f docker-compose.yml ] && [ -d utils ]; then
|
||||
log "Already in a Supabase project directory; skipping bootstrap."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
prepare_source
|
||||
|
||||
target="$(pwd)/$PROJECT_DIR"
|
||||
if [ -e "$target" ]; then
|
||||
die "Target $target already exists. Pick a different name with --project-dir"
|
||||
fi
|
||||
|
||||
log "Creating project at $target"
|
||||
mkdir -p "$target"
|
||||
cp -rf "$SRC_DIR/." "$target/"
|
||||
if [ -f "$target/.env.example" ] && [ ! -f "$target/.env" ]; then
|
||||
cp "$target/.env.example" "$target/.env"
|
||||
fi
|
||||
|
||||
cd "$target"
|
||||
|
||||
current_public_url=$(read_env SUPABASE_PUBLIC_URL)
|
||||
current_site_url=$(read_env SITE_URL)
|
||||
|
||||
[ -z "$current_public_url" ] && current_public_url="http://localhost:8000"
|
||||
[ -z "$current_site_url" ] && current_site_url="http://localhost:3000"
|
||||
|
||||
if [ "$NON_INTERACTIVE" = "1" ]; then
|
||||
log "Non-interactive: using default URLs (edit .env to change)"
|
||||
else
|
||||
echo ""
|
||||
echo "Configure the main URLs (press Enter to accept the default)."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
public_url=$(ask_url "SUPABASE_PUBLIC_URL (Studio + APIs)" "$current_public_url")
|
||||
api_url=$(ask_url "API_EXTERNAL_URL (Auth callbacks)" "$public_url/auth/v1")
|
||||
site_url=$(ask_url "SITE_URL (default Auth redirect)" "$current_site_url")
|
||||
|
||||
# Suggest PROXY_DOMAIN from the public_url host (unless it's localhost-ish)
|
||||
public_host=$(printf '%s' "$public_url" | sed -e 's|^https*://||' -e 's|/.*$||' -e 's|:.*$||')
|
||||
case "$public_host" in
|
||||
localhost|127.*|"") current_proxy_domain=$(read_env PROXY_DOMAIN) ;;
|
||||
*) current_proxy_domain="$public_host" ;;
|
||||
esac
|
||||
[ -z "$current_proxy_domain" ] && current_proxy_domain="your-domain.example.com"
|
||||
|
||||
proxy_domain=$(ask "PROXY_DOMAIN (for nginx/caddy HTTPS proxy)" "$current_proxy_domain")
|
||||
|
||||
# Derive CERTBOT_EMAIL = admin@<last-two-labels-of-proxy-domain>.
|
||||
# Naive: doesn't handle ccTLDs like .co.uk; user can edit .env after.
|
||||
domain_root=$(printf '%s' "$proxy_domain" | awk -F. 'NF>=2 { print $(NF-1)"."$NF; next } { print }')
|
||||
certbot_email="admin@${domain_root}"
|
||||
log "Setting CERTBOT_EMAIL=${certbot_email}"
|
||||
|
||||
sed -i.old \
|
||||
-e "s|^SUPABASE_PUBLIC_URL=.*$|SUPABASE_PUBLIC_URL=${public_url}|" \
|
||||
-e "s|^API_EXTERNAL_URL=.*$|API_EXTERNAL_URL=${api_url}|" \
|
||||
-e "s|^SITE_URL=.*$|SITE_URL=${site_url}|" \
|
||||
-e "s|^PROXY_DOMAIN=.*$|PROXY_DOMAIN=${proxy_domain}|" \
|
||||
-e "s|^CERTBOT_EMAIL=.*$|CERTBOT_EMAIL=${certbot_email}|" \
|
||||
.env
|
||||
rm -f .env.old
|
||||
|
||||
log "Generating secrets and legacy API keys"
|
||||
sh utils/generate-keys.sh --update-env
|
||||
|
||||
log "Generating asymmetric key pair and opaque API keys"
|
||||
sh utils/add-new-auth-keys.sh --update-env
|
||||
|
||||
write_version_stamp "$RESOLVED_REF"
|
||||
|
||||
log "Pulling Docker images"
|
||||
if [ "$NON_INTERACTIVE" = "1" ]; then
|
||||
docker compose --progress quiet pull || warn "docker compose pull failed; you can retry later."
|
||||
else
|
||||
docker compose pull || warn "docker compose pull failed; you can retry later."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Setup complete. Project ready at: $(pwd)"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " cd $(pwd)"
|
||||
echo " sh run.sh config"
|
||||
echo " sh run.sh secrets"
|
||||
echo " sh run.sh start"
|
||||
echo ""
|
||||
echo "To enable docker-compose overrides (caddy, nginx, logs, rustfs, s3, kong):"
|
||||
echo " sh run.sh config add caddy"
|
||||
echo ""
|
||||
@@ -0,0 +1,11 @@
|
||||
# Test override: exposes the S3 backend port for direct testing.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.rustfs.yml \
|
||||
# -f ./tests/docker-compose.rustfs.test.yml up -d
|
||||
#
|
||||
|
||||
services:
|
||||
rustfs:
|
||||
ports:
|
||||
- "${S3_BACKEND_TEST_PORT:-9100}:9000"
|
||||
@@ -0,0 +1,13 @@
|
||||
# Test override: exposes the S3 backend port for direct testing.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml \
|
||||
# -f ./tests/docker-compose.s3.test.yml up -d
|
||||
#
|
||||
# When swapping to a different S3 backend (e.g. RustFS), update the
|
||||
# service name and internal port to match the new backend.
|
||||
|
||||
services:
|
||||
minio:
|
||||
ports:
|
||||
- "${S3_BACKEND_TEST_PORT:-9100}:9000"
|
||||
@@ -0,0 +1,398 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Test API key types and asymmetric auth against a running self-hosted instance.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-auth-keys.sh # Uses http://localhost:8000
|
||||
# sh test-auth-keys.sh <base_url> # Custom URL
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance
|
||||
# - .env file with all keys configured
|
||||
# - jq (for JSON parsing)
|
||||
# - node >= 16 (for HS256 token minting test only)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
BASE_URL="${1:-http://localhost:8000}"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the project directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in jq node; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "Error: $cmd not found."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Read keys from .env
|
||||
JWT_SECRET=$(grep '^JWT_SECRET=' .env | cut -d= -f2-)
|
||||
ANON_KEY=$(grep '^ANON_KEY=' .env | cut -d= -f2-)
|
||||
SERVICE_ROLE_KEY=$(grep '^SERVICE_ROLE_KEY=' .env | cut -d= -f2-)
|
||||
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' .env | cut -d= -f2-)
|
||||
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' .env | cut -d= -f2-)
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name (HTTP $actual)"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
http_status() {
|
||||
url="$1"
|
||||
shift
|
||||
curl -s -o /dev/null -w "%{http_code}" "$@" "$url"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Testing against $BASE_URL ==="
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------
|
||||
# 1. Route tests with API key types
|
||||
# ---------------------------------------------
|
||||
|
||||
echo "--- REST API (/rest/v1/) ---"
|
||||
check "Legacy ANON_KEY -> 403" "403" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $ANON_KEY")"
|
||||
check "Legacy SERVICE_ROLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SERVICE_ROLE_KEY")"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY -> 403" "403" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
|
||||
check "New SECRET_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: $SUPABASE_SECRET_KEY")"
|
||||
else
|
||||
echo " SKIP: Opaque keys not configured"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/rest/v1/")"
|
||||
check "Invalid key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" -H "apikey: invalid-key")"
|
||||
|
||||
echo ""
|
||||
echo "--- Auth (/auth/v1/settings) ---"
|
||||
check "Legacy ANON_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/settings" -H "apikey: $ANON_KEY")"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/settings" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/auth/v1/settings")"
|
||||
|
||||
echo ""
|
||||
echo "--- Storage (/storage/v1/bucket) ---"
|
||||
# Storage has no key-auth - passes through, Storage returns its own errors
|
||||
check "No key -> not 401 (Storage handles auth)" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/storage/v1/bucket")" != "401" ] && echo true || echo false)"
|
||||
check "Legacy ANON_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/storage/v1/bucket" -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY")"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
# With opaque key, the API gateway translates to asymmetric JWT in Authorization
|
||||
check "New PUBLISHABLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/storage/v1/bucket" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- Storage S3 (/storage/v1/s3/) ---"
|
||||
# S3 uses AWS SigV4 auth (not apikey) - the request-transformer Lua expression
|
||||
# passes the Authorization header through unchanged for non-sb_ values
|
||||
check "S3 route accessible" "true" \
|
||||
"$([ "$(http_status "$BASE_URL/storage/v1/s3/")" != "502" ] && echo true || echo false)"
|
||||
|
||||
echo ""
|
||||
echo "--- GraphQL (/graphql/v1) ---"
|
||||
check "Legacy ANON_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/graphql/v1" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY" "200" \
|
||||
"$(http_status "$BASE_URL/graphql/v1" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/graphql/v1" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')"
|
||||
|
||||
echo ""
|
||||
echo "--- Realtime REST (/realtime/v1/api/) ---"
|
||||
# Realtime REST API - use /api/ping to verify key auth (expect 200 with a valid key)
|
||||
check "Legacy ANON_KEY -> 200" "200" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/ping" -H "apikey: $ANON_KEY")"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "New PUBLISHABLE_KEY -> 200" "200" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/ping" -H "apikey: $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
check "No key -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/ping")"
|
||||
|
||||
# Management endpoints must be blocked at the gateway (even with a valid key)
|
||||
check "/api/tenants blocked -> 403" "403" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/tenants" -H "apikey: $ANON_KEY")"
|
||||
check "/api/openapi blocked -> 403" "403" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/openapi" -H "apikey: $ANON_KEY")"
|
||||
|
||||
echo ""
|
||||
echo "--- supabase-js style requests (apikey + Authorization) ---"
|
||||
# supabase-js sends both apikey header AND Authorization: Bearer <apikey>
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "apikey + Authorization: Bearer sb_ (replace path)" "403" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Authorization: Bearer $SUPABASE_PUBLISHABLE_KEY")"
|
||||
|
||||
check "secret apikey + Authorization: Bearer sb_secret" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SUPABASE_SECRET_KEY" \
|
||||
-H "Authorization: Bearer $SUPABASE_SECRET_KEY")"
|
||||
fi
|
||||
|
||||
check "Legacy apikey + Authorization: Bearer <legacy jwt>" "403" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $ANON_KEY")"
|
||||
|
||||
check "Service role apikey + Authorization: Bearer <legacy jwt>" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY")"
|
||||
|
||||
echo ""
|
||||
echo "--- Edge cases ---"
|
||||
# Opaque key in Authorization only (no apikey header) - should be rejected by key-auth
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
check "sb_ in Authorization only (no apikey) -> 401" "401" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "Authorization: Bearer $SUPABASE_PUBLISHABLE_KEY")"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "--- JWKS endpoint ---"
|
||||
check "JWKS public endpoint (no auth)" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/.well-known/jwks.json")"
|
||||
|
||||
# Verify JWKS content: should have EC key, should NOT have symmetric key
|
||||
jwks_content=$(curl -s "$BASE_URL/auth/v1/.well-known/jwks.json")
|
||||
jwks_has_ec=$(echo "$jwks_content" | jq -r '[.keys[] | .kty] | if any(. == "EC") then "true" else "false" end' 2>/dev/null)
|
||||
jwks_has_oct=$(echo "$jwks_content" | jq -r '[.keys[] | .kty] | if any(. == "oct") then "true" else "false" end' 2>/dev/null)
|
||||
check "JWKS contains EC public key" "true" "$jwks_has_ec"
|
||||
check "JWKS does NOT contain symmetric key" "false" "$jwks_has_oct"
|
||||
|
||||
#echo ""
|
||||
#echo "--- OAuth metadata endpoint ---"
|
||||
#check "well-known oauth (no auth)" "200" \
|
||||
# "$(http_status "$BASE_URL/.well-known/oauth-authorization-server")"
|
||||
|
||||
echo ""
|
||||
echo "--- Realtime WebSocket upgrade ---"
|
||||
# Test that WebSocket upgrade request gets through (expect 101 or non-401)
|
||||
# curl --max-time to prevent hanging on successful upgrade (101 keeps connection open)
|
||||
ws_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \
|
||||
"$BASE_URL/realtime/v1/websocket?apikey=$ANON_KEY&vsn=1.0.0" \
|
||||
-H "Upgrade: websocket" \
|
||||
-H "Connection: Upgrade" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||
-H "Sec-WebSocket-Version: 13" 2>/dev/null || echo "000")
|
||||
# 101 = upgrade success, 000 = timeout (connection stayed open = success)
|
||||
check "WebSocket upgrade with legacy key -> not 401" "true" \
|
||||
"$([ "$ws_status" != "401" ] && echo true || echo false)"
|
||||
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
ws_status_new=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \
|
||||
"$BASE_URL/realtime/v1/websocket?apikey=$SUPABASE_PUBLISHABLE_KEY&vsn=1.0.0" \
|
||||
-H "Upgrade: websocket" \
|
||||
-H "Connection: Upgrade" \
|
||||
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
|
||||
-H "Sec-WebSocket-Version: 13" 2>/dev/null || echo "000")
|
||||
check "WebSocket upgrade with opaque key -> not 401" "true" \
|
||||
"$([ "$ws_status_new" != "401" ] && echo true || echo false)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 2. User session JWT tests
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- User session JWT ---"
|
||||
|
||||
# Create user via admin API (works regardless of email autoconfirm setting)
|
||||
test_email="test-keys-$$@example.com"
|
||||
test_password="test-password-123456"
|
||||
|
||||
create_resp=$(curl -s "$BASE_URL/auth/v1/admin/users" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\",\"email_confirm\":true}")
|
||||
|
||||
test_user_id=$(echo "$create_resp" | jq -r '.id // empty' 2>/dev/null)
|
||||
|
||||
# Sign in to get session JWT
|
||||
auth_response=$(curl -s "$BASE_URL/auth/v1/token?grant_type=password" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\"}")
|
||||
|
||||
access_token=$(echo "$auth_response" | jq -r '.access_token // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$access_token" ]; then
|
||||
# Check the algorithm in the JWT header
|
||||
jwt_alg=$(echo "$access_token" | cut -d. -f1 | \
|
||||
jq -Rr '@base64d | fromjson | .alg // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$jwt_alg" ]; then
|
||||
echo " INFO: User session JWT signed with: $jwt_alg"
|
||||
if [ "$jwt_alg" = "ES256" ]; then
|
||||
check "JWT uses ES256 (asymmetric)" "ES256" "$jwt_alg"
|
||||
else
|
||||
check "JWT uses HS256 (legacy)" "HS256" "$jwt_alg"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Use the session JWT with PostgREST
|
||||
check "Session JWT with PostgREST" "403" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
|
||||
check "Session JWT with PostgREST + service role key" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
|
||||
# Use the session JWT with Storage
|
||||
check "Session JWT with Storage" "200" \
|
||||
"$(http_status "$BASE_URL/storage/v1/bucket" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
|
||||
# CRITICAL: Authenticated user + opaque key (most common supabase-js flow)
|
||||
# supabase-js sends apikey: sb_publishable_xxx AND Authorization: Bearer <user_session_jwt>
|
||||
# The expression MUST keep the user JWT and NOT replace it with the anon asymmetric JWT
|
||||
if [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
echo ""
|
||||
echo "--- Authenticated user + opaque key (critical path) ---"
|
||||
check "Opaque apikey + user JWT -> PostgREST uses user JWT" "403" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
check "Secret apikey + user JWT -> PostgREST allowed" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SUPABASE_SECRET_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
check "Opaque apikey + user JWT -> Storage uses user JWT" "200" \
|
||||
"$(http_status "$BASE_URL/storage/v1/bucket" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
check "Opaque apikey + user JWT -> Auth uses user JWT" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/user" \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
fi
|
||||
else
|
||||
check "Sign in test user" "true" "false"
|
||||
fi
|
||||
|
||||
# Clean up test user
|
||||
if [ -n "$test_user_id" ]; then
|
||||
curl -s -o /dev/null "$BASE_URL/auth/v1/admin/users/$test_user_id" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 3. HS256 backward compatibility
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- HS256 backward compatibility ---"
|
||||
|
||||
# Mint a legacy HS256 JWT with role=anon (simulating a pre-migration token)
|
||||
hs256_token=$(JWT_SECRET="$JWT_SECRET" node -e "
|
||||
const crypto = require('crypto');
|
||||
const header = Buffer.from(JSON.stringify({alg:'HS256',typ:'JWT'})).toString('base64url');
|
||||
const payload = Buffer.from(JSON.stringify({
|
||||
role:'anon',iss:'supabase',
|
||||
iat:Math.floor(Date.now()/1000),
|
||||
exp:Math.floor(Date.now()/1000)+3600
|
||||
})).toString('base64url');
|
||||
const sig = crypto.createHmac('sha256',process.env.JWT_SECRET)
|
||||
.update(header+'.'+payload).digest('base64url');
|
||||
console.log(header+'.'+payload+'.'+sig);
|
||||
" 2>/dev/null)
|
||||
|
||||
if [ -n "$hs256_token" ]; then
|
||||
check "HS256 token with PostgREST (backward compat)" "403" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $hs256_token")"
|
||||
check "HS256 token with PostgREST + service role key" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $hs256_token")"
|
||||
else
|
||||
echo " SKIP: Could not mint HS256 token (node required)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 4. JWT_KEYS format validation
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- JWT_KEYS format ---"
|
||||
|
||||
JWT_KEYS_VAL=$(grep '^JWT_KEYS=' .env | cut -d= -f2-)
|
||||
if [ -n "$JWT_KEYS_VAL" ]; then
|
||||
# Auth expects a JSON array, not a JWKS object
|
||||
jwt_keys_is_array=$(echo "$JWT_KEYS_VAL" | jq -r 'if type == "array" then "true" else "false" end' 2>/dev/null)
|
||||
check "JWT_KEYS is JSON array (not JWKS object)" "true" "$jwt_keys_is_array"
|
||||
|
||||
jwt_keys_has_sign=$(echo "$JWT_KEYS_VAL" | jq -r 'if any(.[]; .key_ops and (.key_ops | index("sign"))) then "true" else "false" end' 2>/dev/null)
|
||||
check "JWT_KEYS has a signing key (key_ops: sign)" "true" "$jwt_keys_has_sign"
|
||||
else
|
||||
echo " SKIP: JWT_KEYS not configured"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Verify all self-hosted Supabase services started correctly by checking log output.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-container-logs.sh
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance (docker compose up)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
project_name="${COMPOSE_PROJECT_NAME:-supabase}"
|
||||
|
||||
fail_msg() {
|
||||
fail=$((fail + 1))
|
||||
echo " FAIL: $1"
|
||||
}
|
||||
|
||||
pass_msg() {
|
||||
pass=$((pass + 1))
|
||||
echo " PASS: $1"
|
||||
}
|
||||
|
||||
# The `docker ps` fallback in the helpers below exists because the script
|
||||
# doesn't know which compose `-f` flags the user ran `up` with. `docker compose
|
||||
# ps` only sees services defined in the currently loaded compose files, but
|
||||
# compose stamps `com.docker.compose.{project,service}` labels at `up` time -
|
||||
# so a label-based lookup finds the container regardless of which override
|
||||
# files are active in this shell.
|
||||
|
||||
is_service_running() {
|
||||
service="$1"
|
||||
if docker compose ps --services --status running 2>/dev/null | grep -q "^$service$"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
docker ps --filter "label=com.docker.compose.project=$project_name" \
|
||||
--filter "label=com.docker.compose.service=$service" \
|
||||
--filter "status=running" \
|
||||
--quiet | grep -q '.'
|
||||
}
|
||||
|
||||
get_container_id() {
|
||||
service="$1"
|
||||
|
||||
container_id=$(docker compose ps -q "$service" 2>/dev/null || true)
|
||||
if [ -n "$container_id" ]; then
|
||||
printf '%s' "$container_id"
|
||||
return
|
||||
fi
|
||||
|
||||
container_id=$(docker ps -a \
|
||||
--filter "label=com.docker.compose.project=$project_name" \
|
||||
--filter "label=com.docker.compose.service=$service" \
|
||||
--quiet)
|
||||
|
||||
set -- $container_id
|
||||
printf '%s' "$1"
|
||||
}
|
||||
|
||||
# Check that a service's logs contain all expected patterns.
|
||||
# Logs are written to a temp file so that grep -q exits cleanly.
|
||||
check_logs() {
|
||||
service="$1"
|
||||
shift
|
||||
|
||||
logfile=$(mktemp)
|
||||
|
||||
docker compose logs "$service" > "$logfile" 2>/dev/null || true
|
||||
if [ ! -s "$logfile" ]; then
|
||||
container_id=$(get_container_id "$service")
|
||||
if [ -n "$container_id" ]; then
|
||||
docker logs "$container_id" > "$logfile" 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -s "$logfile" ]; then
|
||||
rm -f "$logfile"
|
||||
fail_msg "$service (no logs found)"
|
||||
return
|
||||
fi
|
||||
|
||||
for pattern in "$@"; do
|
||||
if ! grep -q -i -E "$pattern" "$logfile"; then
|
||||
rm -f "$logfile"
|
||||
fail_msg "$service (missing: $pattern)"
|
||||
return
|
||||
fi
|
||||
done
|
||||
|
||||
rm -f "$logfile"
|
||||
pass_msg "$service"
|
||||
}
|
||||
|
||||
check_logs_if_running() {
|
||||
service="$1"
|
||||
shift
|
||||
|
||||
if is_service_running "$service"; then
|
||||
check_logs "$service" "$@"
|
||||
else
|
||||
pass_msg "$service (skipped: service not running)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Checking service startup logs ==="
|
||||
echo ""
|
||||
|
||||
check_logs db \
|
||||
'PostgreSQL init process complete; ready for start up.|Skipping initialization'
|
||||
|
||||
check_logs auth \
|
||||
'db worker started'
|
||||
|
||||
# API gateway: Envoy by default, or Kong when the kong override is enabled.
|
||||
# The service is named api-gw in both cases, so accept either startup marker.
|
||||
check_logs api-gw \
|
||||
'init\.lua.*declarative config loaded|Envoy configuration generated successfully'
|
||||
|
||||
check_logs rest \
|
||||
'Schema cache loaded in.*milliseconds'
|
||||
|
||||
check_logs realtime \
|
||||
'Starting Realtime' \
|
||||
'Connected to Postgres database' \
|
||||
'Janitor started' \
|
||||
'Starting MetricsCleaner'
|
||||
|
||||
check_logs storage \
|
||||
'Started Successfully'
|
||||
|
||||
check_logs studio \
|
||||
'ready in.*s$'
|
||||
|
||||
check_logs meta \
|
||||
'Server listening at http'
|
||||
|
||||
check_logs functions \
|
||||
'main function started'
|
||||
|
||||
check_logs_if_running analytics \
|
||||
'Access LogflareWeb.Endpoint at http://localhost:4000' \
|
||||
'Executing startup tasks' \
|
||||
'Ensuring single tenant user is seeded'
|
||||
|
||||
# Database pooler: Supavisor by default, or PgBouncer when the pgbouncer
|
||||
# override is enabled (which disables Supavisor). Check whichever is running.
|
||||
if is_service_running supavisor; then
|
||||
check_logs supavisor \
|
||||
'Connected to Postgres database' \
|
||||
'HEAD /api/health$'
|
||||
elif is_service_running pgbouncer; then
|
||||
check_logs pgbouncer \
|
||||
'process up: PgBouncer' \
|
||||
'listening on .*6432'
|
||||
else
|
||||
fail_msg "pooler (neither supavisor nor pgbouncer is running)"
|
||||
fi
|
||||
|
||||
check_logs_if_running vector \
|
||||
'Vector has started'
|
||||
|
||||
check_logs imgproxy \
|
||||
'Starting server at :5001'
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
echo "Inspect logs: docker compose logs <service>"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,355 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Test Postgres 15 -> 17 upgrade for self-hosted Supabase.
|
||||
#
|
||||
# Seeds test data on a running Postgres 15 stack, runs the upgrade script,
|
||||
# and verifies data integrity + service connectivity using pgTAP.
|
||||
#
|
||||
# Usage:
|
||||
# cd docker/
|
||||
# sudo bash tests/test-pg17-upgrade.sh
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase with a clean, tests-only Postgres 15.
|
||||
# Postgres 17 is now the default, so start PG 15 explicitly via the override:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.pg15.yml up -d
|
||||
# - .env file with POSTGRES_PASSWORD, ANON_KEY
|
||||
#
|
||||
|
||||
set -eu
|
||||
|
||||
DB_CONTAINER="supabase-db"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the docker/ directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pg_password=$(grep '^POSTGRES_PASSWORD=' .env | cut -d '=' -f 2-)
|
||||
anon_key=$(grep '^ANON_KEY=' .env | cut -d '=' -f 2- || true)
|
||||
|
||||
if [ -z "$pg_password" ]; then
|
||||
echo "Error: POSTGRES_PASSWORD not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_sql() {
|
||||
docker exec -i \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$DB_CONTAINER" \
|
||||
psql -h localhost -U supabase_admin -d postgres -v ON_ERROR_STOP=1 "$@"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Postgres 15 -> 17 Upgrade Test ==="
|
||||
echo ""
|
||||
|
||||
# --- Verify we're starting from Postgres 15 --------------------------------
|
||||
|
||||
current_version=$(run_sql -A -t -c "SHOW server_version;" | head -1)
|
||||
case "$current_version" in
|
||||
15.*) echo "Starting version: PostgreSQL $current_version" ;;
|
||||
17.*) echo "Error: Already on Postgres 17. Start with a PG 15 stack."; exit 1 ;;
|
||||
*) echo "Error: Unexpected version: $current_version"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# --- Seed test data --------------------------------------------------------
|
||||
# Note: this script is designed to run against a fresh docker-compose stack,
|
||||
# not an existing database with user data.
|
||||
|
||||
echo ""
|
||||
echo "Seeding test data on Postgres 15..."
|
||||
|
||||
run_sql <<'EOSQL'
|
||||
-- Test table with various column types
|
||||
CREATE TABLE IF NOT EXISTS public._upgrade_test (
|
||||
id serial PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
value numeric(10,2),
|
||||
created_at timestamptz DEFAULT now(),
|
||||
metadata jsonb
|
||||
);
|
||||
|
||||
TRUNCATE public._upgrade_test;
|
||||
INSERT INTO public._upgrade_test (name, value, metadata) VALUES
|
||||
('alpha', 1.50, '{"tag": "a"}'),
|
||||
('bravo', 2.75, '{"tag": "b"}'),
|
||||
('charlie', 3.00, '{"tag": "c"}'),
|
||||
('delta', 4.25, '{"tag": "d"}'),
|
||||
('echo', 5.99, '{"tag": "e"}');
|
||||
|
||||
-- Index
|
||||
CREATE INDEX IF NOT EXISTS _upgrade_test_name_idx ON public._upgrade_test (name);
|
||||
|
||||
-- Function
|
||||
CREATE OR REPLACE FUNCTION public._upgrade_test_fn(n int)
|
||||
RETURNS int LANGUAGE sql IMMUTABLE AS $$
|
||||
SELECT n * 2;
|
||||
$$;
|
||||
|
||||
-- Grant access so PostgREST can read it
|
||||
GRANT SELECT ON public._upgrade_test TO anon, authenticated;
|
||||
|
||||
-- pg_cron: created here as supabase_admin (the common manually-enabled case),
|
||||
-- so the extension is owned by supabase_admin, NOT postgres. complete.sh's
|
||||
-- drop+recreate only fires for postgres-owned pg_cron, so this exercises the
|
||||
-- version reconcile in the upgrade script: Supabase PG 15 registers pg_cron
|
||||
-- as '1.6', while the target image packages it as '1.6.4' with no update
|
||||
-- path between them.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_cron;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'upgrade_test_job') THEN
|
||||
PERFORM cron.unschedule('upgrade_test_job');
|
||||
END IF;
|
||||
END $$;
|
||||
SELECT cron.schedule('upgrade_test_job', '5 4 * * *', 'SELECT 1');
|
||||
EOSQL
|
||||
|
||||
pre_count=$(run_sql -A -t -c "SELECT count(*) FROM public._upgrade_test;" | tr -d '[:space:]')
|
||||
pre_checksum=$(run_sql -A -t -c "SELECT md5(string_agg(name || value::text, ',' ORDER BY id)) FROM public._upgrade_test;" | tr -d '[:space:]')
|
||||
|
||||
echo " Rows: $pre_count"
|
||||
echo " Checksum: $pre_checksum"
|
||||
|
||||
# --- Seed a Vault secret ---------------------------------------------------
|
||||
# Verifies the pgsodium root key survives the volume swap/chown AND that Vault
|
||||
# secrets still decrypt on Postgres 17. For legacy (key_id-based) secrets this
|
||||
# also exercises complete.sh's pgsodium->Vault re-encryption; on a stock
|
||||
# self-hosted stack the secret is already pgsodium-less, so this confirms the
|
||||
# round-trip and the post-upgrade invariant (key_id IS NULL).
|
||||
VAULT_SECRET_NAME="upgrade_test_secret"
|
||||
VAULT_SECRET_VALUE="upgrade-test-secret-value-42"
|
||||
vault_available="f"
|
||||
if [ "$(run_sql -A -t -c "SELECT EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'supabase_vault');" | tr -d '[:space:]')" = "t" ]; then
|
||||
echo ""
|
||||
echo "Seeding Vault secret on Postgres 15..."
|
||||
run_sql <<EOSQL
|
||||
CREATE EXTENSION IF NOT EXISTS supabase_vault;
|
||||
-- Pre-clean so a re-run after a mid-run failure (where the trailing cleanup
|
||||
-- never executed) does not abort on the unique (name) index.
|
||||
DELETE FROM vault.secrets WHERE name = '${VAULT_SECRET_NAME}';
|
||||
SELECT vault.create_secret('${VAULT_SECRET_VALUE}', '${VAULT_SECRET_NAME}', 'pg17 upgrade test');
|
||||
EOSQL
|
||||
pre_secret=$(run_sql -A -t -c "SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = '${VAULT_SECRET_NAME}';" | tr -d '\n')
|
||||
if [ "$pre_secret" = "$VAULT_SECRET_VALUE" ]; then
|
||||
echo " Seeded '${VAULT_SECRET_NAME}' (decrypts correctly on PG 15)"
|
||||
else
|
||||
echo " Warning: seeded secret did not decrypt as expected on PG 15" >&2
|
||||
fi
|
||||
vault_available="t"
|
||||
else
|
||||
echo ""
|
||||
echo "Skipping Vault seed: supabase_vault extension not available."
|
||||
fi
|
||||
|
||||
# --- Run upgrade -----------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "Running upgrade script..."
|
||||
echo ""
|
||||
|
||||
bash utils/upgrade-pg17.sh --yes
|
||||
|
||||
echo ""
|
||||
|
||||
# --- Verify with pgTAP ----------------------------------------------------
|
||||
|
||||
echo "Running pgTAP verification..."
|
||||
echo ""
|
||||
|
||||
# Optional Vault assertions, only when a secret was seeded above.
|
||||
vault_plan=0
|
||||
vault_tests=""
|
||||
if [ "$vault_available" = "t" ]; then
|
||||
vault_plan=3
|
||||
vault_tests=$(cat <<EOSQL
|
||||
-- Vault secret survived the upgrade and still decrypts (root key intact).
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM vault.secrets WHERE name = '${VAULT_SECRET_NAME}'),
|
||||
'Vault secret survived upgrade'
|
||||
);
|
||||
SELECT is(
|
||||
(SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = '${VAULT_SECRET_NAME}'),
|
||||
'${VAULT_SECRET_VALUE}',
|
||||
'Vault secret still decrypts to original plaintext after upgrade'
|
||||
);
|
||||
SELECT ok(
|
||||
(SELECT key_id IS NULL FROM vault.secrets WHERE name = '${VAULT_SECRET_NAME}'),
|
||||
'Vault secret is in pgsodium-less format (key_id IS NULL) after upgrade'
|
||||
);
|
||||
EOSQL
|
||||
)
|
||||
fi
|
||||
total_plan=$((16 + vault_plan))
|
||||
|
||||
# Use a non-quoted heredoc so $pre_count, $pre_checksum, $total_plan and
|
||||
# $vault_tests are interpolated.
|
||||
run_sql <<EOSQL
|
||||
CREATE EXTENSION IF NOT EXISTS pgtap;
|
||||
|
||||
SELECT plan(${total_plan});
|
||||
|
||||
-- Version
|
||||
SELECT ok(version() LIKE 'PostgreSQL 17%', 'Running Postgres 17');
|
||||
|
||||
-- Table
|
||||
SELECT has_table('public', '_upgrade_test', 'Test table survived upgrade');
|
||||
|
||||
-- Row count
|
||||
SELECT is(
|
||||
(SELECT count(*)::int FROM public._upgrade_test),
|
||||
${pre_count},
|
||||
'Row count preserved'
|
||||
);
|
||||
|
||||
-- Data checksum
|
||||
SELECT is(
|
||||
(SELECT md5(string_agg(name || value::text, ',' ORDER BY id)) FROM public._upgrade_test),
|
||||
'${pre_checksum}',
|
||||
'Data checksum matches'
|
||||
);
|
||||
|
||||
-- Index
|
||||
SELECT has_index('public', '_upgrade_test', '_upgrade_test_name_idx', 'Index survived upgrade');
|
||||
|
||||
-- Function
|
||||
SELECT has_function('public', '_upgrade_test_fn', ARRAY['integer'], 'Function survived upgrade');
|
||||
SELECT is(public._upgrade_test_fn(21), 42, 'Function returns correct result');
|
||||
|
||||
-- Core extensions
|
||||
-- Note: pgsodium may not be created as an extension in the postgres database
|
||||
-- on default self-hosted installs (it's loaded via shared_preload_libraries
|
||||
-- but the CREATE EXTENSION is conditional in the init migration).
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_net'),
|
||||
'pg_net extension exists'
|
||||
);
|
||||
|
||||
-- Roles
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_etl_admin'),
|
||||
'supabase_etl_admin role exists'
|
||||
);
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_read_only_user'),
|
||||
'supabase_read_only_user role exists'
|
||||
);
|
||||
|
||||
-- postgres is not superuser
|
||||
SELECT ok(
|
||||
NOT (SELECT rolsuper FROM pg_roles WHERE rolname = 'postgres'),
|
||||
'postgres role is not superuser'
|
||||
);
|
||||
|
||||
-- pg_cron: registered as 1.6 on PG 15; the upgrade must reconcile the version
|
||||
-- label to the target's packaged 1.6.4 and preserve scheduled jobs.
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron'),
|
||||
'pg_cron extension exists'
|
||||
);
|
||||
SELECT is(
|
||||
(SELECT extversion FROM pg_extension WHERE extname = 'pg_cron'),
|
||||
'1.6.4',
|
||||
'pg_cron version label reconciled to 1.6.4'
|
||||
);
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'upgrade_test_job'),
|
||||
'pg_cron scheduled job survived the upgrade'
|
||||
);
|
||||
|
||||
-- New predefined role (initdb-only migration; the target image's supautils.conf
|
||||
-- references it, so it must exist on an upgraded instance).
|
||||
SELECT ok(
|
||||
EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_privileged_role'),
|
||||
'supabase_privileged_role exists'
|
||||
);
|
||||
|
||||
-- Extension version reconcile: complete.sh ran with .063 binaries, so the
|
||||
-- catalog should be reconciled to the target image's default version.
|
||||
SELECT is(
|
||||
(SELECT extversion FROM pg_extension WHERE extname = 'pg_net'),
|
||||
(SELECT default_version FROM pg_available_extensions WHERE name = 'pg_net'),
|
||||
'pg_net version reconciled to image default'
|
||||
);
|
||||
${vault_tests}
|
||||
SELECT * FROM finish(true);
|
||||
EOSQL
|
||||
|
||||
# --- Check service connectivity --------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "Checking service connectivity..."
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# PostgREST
|
||||
if [ -n "$anon_key" ]; then
|
||||
rest_status=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "apikey: $anon_key" \
|
||||
-H "Authorization: Bearer $anon_key" \
|
||||
"http://localhost:8000/rest/v1/_upgrade_test?select=count" 2>/dev/null) || rest_status="000"
|
||||
check "PostgREST connectivity" "200" "$rest_status"
|
||||
fi
|
||||
|
||||
# Auth health (needs apikey header through the API gateway)
|
||||
if [ -n "$anon_key" ]; then
|
||||
auth_status=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "apikey: $anon_key" \
|
||||
"http://localhost:8000/auth/v1/health" 2>/dev/null) || auth_status="000"
|
||||
check "Auth service health" "200" "$auth_status"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Services: $pass passed, $fail failed"
|
||||
|
||||
# --- Clean up test artifacts ----------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "Cleaning up test artifacts..."
|
||||
|
||||
run_sql <<'EOSQL' || true
|
||||
DROP FUNCTION IF EXISTS public._upgrade_test_fn(int);
|
||||
DROP TABLE IF EXISTS public._upgrade_test;
|
||||
DROP EXTENSION IF EXISTS pgtap;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM cron.job WHERE jobname = 'upgrade_test_job') THEN
|
||||
PERFORM cron.unschedule('upgrade_test_job');
|
||||
END IF;
|
||||
END $$;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'supabase_vault') THEN
|
||||
DELETE FROM vault.secrets WHERE name = 'upgrade_test_secret';
|
||||
END IF;
|
||||
END $$;
|
||||
EOSQL
|
||||
|
||||
# --- Summary --------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
echo "=== SOME TESTS FAILED ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Upgrade test passed ==="
|
||||
echo ""
|
||||
echo "To reclaim disk space:"
|
||||
echo " rm -rf ./volumes/db/data.bak.pg15 ./volumes/db/pg17_upgrade_bin_*.tar.gz"
|
||||
echo ""
|
||||
@@ -0,0 +1,371 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Test S3 backend directly, bypassing the Storage service.
|
||||
#
|
||||
# Validates that the S3-compatible backend (MinIO, RustFS, etc.) handles
|
||||
# all S3 operations that Storage relies on. Uses the aws cli so the test
|
||||
# is backend-agnostic - no vendor-specific tools required.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-s3-backend.sh # Uses localhost:9100
|
||||
# sh test-s3-backend.sh <backend_url> # Custom URL
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance with S3 backend + test override:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml \
|
||||
# -f ./tests/docker-compose.s3.test.yml up -d
|
||||
# - .env file with MINIO_ROOT_USER, MINIO_ROOT_PASSWORD, GLOBAL_S3_BUCKET
|
||||
# - aws cli v2
|
||||
# - jq (for JSON parsing)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cleanup_files=""
|
||||
trap 'rm -f $cleanup_files' EXIT
|
||||
|
||||
BACKEND_URL="${1:-http://localhost:${S3_BACKEND_TEST_PORT:-9100}}"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the project directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in aws jq; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "Error: $cmd not found."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Read backend credentials from .env
|
||||
BACKEND_ACCESS_KEY=$(grep '^MINIO_ROOT_USER=' .env | cut -d= -f2-)
|
||||
BACKEND_SECRET_KEY=$(grep '^MINIO_ROOT_PASSWORD=' .env | cut -d= -f2-)
|
||||
GLOBAL_S3_BUCKET=$(grep '^GLOBAL_S3_BUCKET=' .env | cut -d= -f2-)
|
||||
REGION=$(grep '^REGION=' .env | cut -d= -f2-)
|
||||
REGION="${REGION:-us-east-1}"
|
||||
|
||||
if [ -z "$BACKEND_ACCESS_KEY" ] || [ -z "$BACKEND_SECRET_KEY" ]; then
|
||||
echo "Error: MINIO_ROOT_USER or MINIO_ROOT_PASSWORD not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Wrapper for aws commands against the backend directly.
|
||||
#
|
||||
# Always exits 0: under `set -e` a failing aws call would kill the suite on the
|
||||
# spot, so the check never records a FAIL and no summary is printed. The aws
|
||||
# error is echoed to stderr so a FAIL stays explainable even where the caller
|
||||
# discards stdout.
|
||||
s3() {
|
||||
s3_out=$(AWS_ACCESS_KEY_ID="$BACKEND_ACCESS_KEY" \
|
||||
AWS_SECRET_ACCESS_KEY="$BACKEND_SECRET_KEY" \
|
||||
aws "$@" --endpoint-url "$BACKEND_URL" --region "$REGION" 2>&1) ||
|
||||
echo " aws $1 $2 failed: $(printf '%s' "$s3_out" | tail -n 1)" >&2
|
||||
printf '%s\n' "$s3_out"
|
||||
}
|
||||
|
||||
# Wrapper for jq that yields empty output instead of aborting the suite when
|
||||
# the payload is not JSON (e.g. an S3 error document) and jq exits non-zero.
|
||||
jq_r() {
|
||||
jq -r "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
bucket_name="backend-test-$$"
|
||||
|
||||
echo ""
|
||||
echo "=== S3 backend test against $BACKEND_URL ==="
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------
|
||||
# 1. ListBuckets (backend reachable)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo "--- Connectivity ---"
|
||||
list_output=$(s3 s3api list-buckets --output json)
|
||||
list_ok=$(echo "$list_output" | jq_r 'if .Buckets then "true" else "false" end')
|
||||
check "Backend reachable (ListBuckets)" "true" "$list_ok"
|
||||
|
||||
if [ "$list_ok" != "true" ]; then
|
||||
echo " Cannot reach backend. Is the test override running?"
|
||||
echo " Response: $list_output"
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 2. Verify GLOBAL_S3_BUCKET exists
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Storage bucket ---"
|
||||
if [ -n "$GLOBAL_S3_BUCKET" ]; then
|
||||
storage_bucket_exists=$(s3 s3api list-buckets --output json | \
|
||||
jq_r --arg name "$GLOBAL_S3_BUCKET" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end')
|
||||
check "GLOBAL_S3_BUCKET ($GLOBAL_S3_BUCKET) exists" "true" "$storage_bucket_exists"
|
||||
else
|
||||
echo " SKIP: GLOBAL_S3_BUCKET not set"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 3. CreateBucket
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- CreateBucket ---"
|
||||
s3 s3api create-bucket --bucket "$bucket_name" --output json >/dev/null
|
||||
|
||||
# Verify create succeeded
|
||||
create_found=$(s3 s3api list-buckets --output json | \
|
||||
jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end')
|
||||
check "CreateBucket" "true" "$create_found"
|
||||
|
||||
if [ "$create_found" != "true" ]; then
|
||||
echo " Cannot continue without a bucket. Aborting."
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify in ListBuckets (separate call)
|
||||
bucket_found=$(s3 s3api list-buckets --output json | \
|
||||
jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end')
|
||||
check "Bucket visible in ListBuckets" "true" "$bucket_found"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 4. PutObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- PutObject ---"
|
||||
tmpfile=$(mktemp); cleanup_files="$cleanup_files $tmpfile"
|
||||
echo "hello from backend test" > "$tmpfile"
|
||||
put_output=$(s3 s3 cp "$tmpfile" "s3://$bucket_name/test-file.txt")
|
||||
put_ok=$(echo "$put_output" | grep -q "upload:" && echo "true" || echo "false")
|
||||
check "PutObject" "true" "$put_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 5. ListObjectsV2
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- ListObjectsV2 ---"
|
||||
list_objects=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json)
|
||||
object_found=$(echo "$list_objects" | \
|
||||
jq_r '[.Contents[]? | .Key] | if any(. == "test-file.txt") then "true" else "false" end')
|
||||
check "Object found in ListObjectsV2" "true" "$object_found"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 6. HeadObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- HeadObject ---"
|
||||
head_output=$(s3 s3api head-object --bucket "$bucket_name" --key "test-file.txt" --output json)
|
||||
head_size=$(echo "$head_output" | jq_r '.ContentLength // 0')
|
||||
original_size=$(wc -c < "$tmpfile" | tr -d ' ')
|
||||
check "HeadObject returns correct size" "$original_size" "$head_size"
|
||||
rm -f "$tmpfile"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 7. GetObject + content verify
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- GetObject ---"
|
||||
download_file=$(mktemp); cleanup_files="$cleanup_files $download_file"
|
||||
s3 s3 cp "s3://$bucket_name/test-file.txt" "$download_file" >/dev/null
|
||||
downloaded_content=$(cat "$download_file")
|
||||
check "GetObject content matches" "hello from backend test" "$downloaded_content"
|
||||
rm -f "$download_file"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 8. CopyObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- CopyObject ---"
|
||||
copy_output=$(s3 s3 cp "s3://$bucket_name/test-file.txt" "s3://$bucket_name/test-copy.txt")
|
||||
copy_ok=$(echo "$copy_output" | grep -q "copy:" && echo "true" || echo "false")
|
||||
check "CopyObject" "true" "$copy_ok"
|
||||
|
||||
copy_download=$(mktemp); cleanup_files="$cleanup_files $copy_download"
|
||||
s3 s3 cp "s3://$bucket_name/test-copy.txt" "$copy_download" >/dev/null
|
||||
check "Copied object content matches" "hello from backend test" "$(cat "$copy_download")"
|
||||
rm -f "$copy_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 9. DeleteObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- DeleteObject ---"
|
||||
s3 s3 rm "s3://$bucket_name/test-copy.txt" >/dev/null
|
||||
list_after_delete=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json)
|
||||
copy_gone=$(echo "$list_after_delete" | \
|
||||
jq_r '[.Contents[]? | .Key] | if any(. == "test-copy.txt") then "false" else "true" end')
|
||||
check "Deleted object no longer listed" "true" "$copy_gone"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 10. Multipart upload (7MB)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Multipart upload (7MB) ---"
|
||||
large_file=$(mktemp); cleanup_files="$cleanup_files $large_file"
|
||||
dd if=/dev/urandom of="$large_file" bs=1048576 count=7 2>/dev/null
|
||||
large_size=$(wc -c < "$large_file" | tr -d ' ')
|
||||
large_put=$(s3 s3 cp "$large_file" "s3://$bucket_name/large-file.bin")
|
||||
large_ok=$(echo "$large_put" | grep -q "upload:" && echo "true" || echo "false")
|
||||
check "Multipart upload (7MB)" "true" "$large_ok"
|
||||
|
||||
large_head=$(s3 s3api head-object --bucket "$bucket_name" --key "large-file.bin" --output json)
|
||||
remote_size=$(echo "$large_head" | jq_r '.ContentLength // 0')
|
||||
check "Multipart size matches ($large_size bytes)" "$large_size" "$remote_size"
|
||||
|
||||
large_download=$(mktemp); cleanup_files="$cleanup_files $large_download"
|
||||
s3 s3 cp "s3://$bucket_name/large-file.bin" "$large_download" >/dev/null
|
||||
download_size=$(wc -c < "$large_download" | tr -d ' ')
|
||||
check "Multipart download size matches" "$large_size" "$download_size"
|
||||
rm -f "$large_file" "$large_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 11. DeleteObjects (batch delete)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- DeleteObjects (batch) ---"
|
||||
batch_file=$(mktemp); cleanup_files="$cleanup_files $batch_file"
|
||||
echo "batch-a" > "$batch_file"
|
||||
s3 s3 cp "$batch_file" "s3://$bucket_name/batch-a.txt" >/dev/null
|
||||
echo "batch-b" > "$batch_file"
|
||||
s3 s3 cp "$batch_file" "s3://$bucket_name/batch-b.txt" >/dev/null
|
||||
echo "batch-c" > "$batch_file"
|
||||
s3 s3 cp "$batch_file" "s3://$bucket_name/batch-c.txt" >/dev/null
|
||||
rm -f "$batch_file"
|
||||
delete_objects_output=$(s3 s3api delete-objects --bucket "$bucket_name" \
|
||||
--delete '{"Objects":[{"Key":"batch-a.txt"},{"Key":"batch-b.txt"},{"Key":"batch-c.txt"}]}' \
|
||||
--output json)
|
||||
deleted_count=$(echo "$delete_objects_output" | jq_r '.Deleted | length')
|
||||
check "DeleteObjects removed 3 objects" "3" "$deleted_count"
|
||||
|
||||
# Verify all gone
|
||||
batch_list=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --prefix "batch-" --output json)
|
||||
batch_remaining=$(echo "$batch_list" | jq_r '[.Contents[]?] | length')
|
||||
check "Batch-deleted objects gone" "0" "$batch_remaining"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 12. Presigned URLs
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Presigned URLs ---"
|
||||
presign_file=$(mktemp); cleanup_files="$cleanup_files $presign_file"
|
||||
echo "presigned content test" > "$presign_file"
|
||||
s3 s3 cp "$presign_file" "s3://$bucket_name/presign-test.txt" >/dev/null
|
||||
rm -f "$presign_file"
|
||||
|
||||
presigned_url=$(s3 s3 presign "s3://$bucket_name/presign-test.txt")
|
||||
presign_body=$(curl -s "$presigned_url" || true)
|
||||
check "Presigned URL returns correct content" "presigned content test" "$presign_body"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 13. Conditional request (IfNoneMatch)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Conditional request (IfNoneMatch) ---"
|
||||
# --if-none-match requires aws cli v2.22+ ; skip if not supported
|
||||
if aws s3api put-object help 2>&1 | grep -q 'if-none-match'; then
|
||||
cond_file=$(mktemp); cleanup_files="$cleanup_files $cond_file"
|
||||
echo "conditional test" > "$cond_file"
|
||||
|
||||
# First put should succeed (key doesn't exist)
|
||||
first_put_err=$(s3 s3api put-object --bucket "$bucket_name" --key "cond-test.txt" \
|
||||
--body "$cond_file" --if-none-match '*' --output json 2>&1 || true)
|
||||
first_put_ok=$(echo "$first_put_err" | grep -qi "error\|denied\|PreconditionFailed" && echo "false" || echo "true")
|
||||
check "IfNoneMatch put (new key) succeeds" "true" "$first_put_ok"
|
||||
|
||||
# Second put should fail with PreconditionFailed (key exists)
|
||||
cond_err=$(s3 s3api put-object --bucket "$bucket_name" --key "cond-test.txt" \
|
||||
--body "$cond_file" --if-none-match '*' --output json 2>&1 || true)
|
||||
cond_rejected=$(echo "$cond_err" | grep -qi "PreconditionFailed" && echo "true" || echo "false")
|
||||
check "IfNoneMatch put (existing key) rejected" "true" "$cond_rejected"
|
||||
rm -f "$cond_file"
|
||||
else
|
||||
echo " SKIP: aws cli does not support --if-none-match (requires v2.22+)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 14. Range request (partial GetObject)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Range request ---"
|
||||
range_file=$(mktemp); cleanup_files="$cleanup_files $range_file"
|
||||
echo "hello range test content" > "$range_file"
|
||||
s3 s3 cp "$range_file" "s3://$bucket_name/range-test.txt" >/dev/null
|
||||
rm -f "$range_file"
|
||||
|
||||
range_download=$(mktemp); cleanup_files="$cleanup_files $range_download"
|
||||
s3 s3api get-object --bucket "$bucket_name" --key "range-test.txt" \
|
||||
--range "bytes=0-4" "$range_download" --output json >/dev/null
|
||||
range_content=$(cat "$range_download")
|
||||
check "Range request returns partial content" "hello" "$range_content"
|
||||
rm -f "$range_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 15. Authentication
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Authentication ---"
|
||||
bad_output=$(AWS_ACCESS_KEY_ID="invalid-key" \
|
||||
AWS_SECRET_ACCESS_KEY="invalid-secret" \
|
||||
aws s3api list-buckets \
|
||||
--endpoint-url "$BACKEND_URL" \
|
||||
--region "$REGION" \
|
||||
--output json 2>&1 || true)
|
||||
bad_ok=$(echo "$bad_output" | grep -qi "denied\|invalid\|error\|403\|401" && echo "true" || echo "false")
|
||||
check "Invalid credentials rejected" "true" "$bad_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 16. Cleanup
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Cleanup ---"
|
||||
s3 s3 rm "s3://$bucket_name/" --recursive >/dev/null
|
||||
s3 s3api delete-bucket --bucket "$bucket_name" >/dev/null
|
||||
bucket_gone=$(s3 s3api list-buckets --output json | \
|
||||
jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "false" else "true" end')
|
||||
check "Test bucket deleted" "true" "$bucket_gone"
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,301 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Test S3 protocol endpoint for self-hosted Supabase Storage.
|
||||
#
|
||||
# Verifies that the S3-compatible endpoint at /storage/v1/s3 works with
|
||||
# standard S3 clients - the same way end users interact with it via
|
||||
# aws cli, rclone, or other S3-compatible tools.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-s3.sh # Uses http://localhost:8000
|
||||
# sh test-s3.sh <base_url> # Custom URL
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance with S3 enabled:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d
|
||||
# - .env file with S3_PROTOCOL_ACCESS_KEY_ID, S3_PROTOCOL_ACCESS_KEY_SECRET, REGION
|
||||
# - aws cli v2 (for S3 operations)
|
||||
# - jq (for JSON parsing)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cleanup_files=""
|
||||
trap 'rm -f $cleanup_files' EXIT
|
||||
|
||||
BASE_URL="${1:-http://localhost:8000}"
|
||||
S3_ENDPOINT="$BASE_URL/storage/v1/s3"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the project directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for cmd in aws jq; do
|
||||
if ! command -v $cmd >/dev/null 2>&1; then
|
||||
echo "Error: $cmd not found."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Read keys from .env
|
||||
S3_ACCESS_KEY=$(grep '^S3_PROTOCOL_ACCESS_KEY_ID=' .env | cut -d= -f2-)
|
||||
S3_SECRET_KEY=$(grep '^S3_PROTOCOL_ACCESS_KEY_SECRET=' .env | cut -d= -f2-)
|
||||
REGION=$(grep '^REGION=' .env | cut -d= -f2-)
|
||||
|
||||
if [ -z "$S3_ACCESS_KEY" ] || [ -z "$S3_SECRET_KEY" ]; then
|
||||
echo "Error: S3_PROTOCOL_ACCESS_KEY_ID or S3_PROTOCOL_ACCESS_KEY_SECRET not set in .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Wrapper for aws s3/s3api commands with correct endpoint and credentials.
|
||||
#
|
||||
# Always exits 0: under `set -e` a failing aws call would kill the suite on the
|
||||
# spot, so the check never records a FAIL and no summary is printed. The aws
|
||||
# error is echoed to stderr so a FAIL stays explainable even where the caller
|
||||
# discards stdout.
|
||||
s3() {
|
||||
s3_out=$(AWS_ACCESS_KEY_ID="$S3_ACCESS_KEY" \
|
||||
AWS_SECRET_ACCESS_KEY="$S3_SECRET_KEY" \
|
||||
aws "$@" --endpoint-url "$S3_ENDPOINT" --region "$REGION" 2>&1) ||
|
||||
echo " aws $1 $2 failed: $(printf '%s' "$s3_out" | tail -n 1)" >&2
|
||||
printf '%s\n' "$s3_out"
|
||||
}
|
||||
|
||||
# Wrapper for jq that yields empty output instead of aborting the suite when
|
||||
# the payload is not JSON (e.g. an S3 error document) and jq exits non-zero.
|
||||
jq_r() {
|
||||
jq -r "$@" 2>/dev/null || true
|
||||
}
|
||||
|
||||
bucket_name="s3-test-$$"
|
||||
|
||||
echo ""
|
||||
echo "=== S3 protocol test against $BASE_URL ==="
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------
|
||||
# 1. S3 ListBuckets
|
||||
# ---------------------------------------------
|
||||
|
||||
echo "--- S3 ListBuckets ---"
|
||||
list_output=$(s3 s3api list-buckets --output json)
|
||||
list_ok=$(echo "$list_output" | jq_r 'if .Buckets then "true" else "false" end')
|
||||
check "ListBuckets returns valid response" "true" "$list_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 2. S3 CreateBucket
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 CreateBucket ---"
|
||||
s3 s3api create-bucket --bucket "$bucket_name" --output json >/dev/null
|
||||
|
||||
# Verify create succeeded
|
||||
create_found=$(s3 s3api list-buckets --output json | \
|
||||
jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end')
|
||||
check "CreateBucket" "true" "$create_found"
|
||||
|
||||
if [ "$create_found" != "true" ]; then
|
||||
echo " Cannot continue without a bucket. Aborting."
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify bucket appears in ListBuckets (separate call)
|
||||
s3_bucket_found=$(s3 s3api list-buckets --output json | \
|
||||
jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "true" else "false" end')
|
||||
check "Bucket visible in ListBuckets" "true" "$s3_bucket_found"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 3. S3 PutObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 PutObject ---"
|
||||
tmpfile=$(mktemp); cleanup_files="$cleanup_files $tmpfile"
|
||||
echo "hello from s3 upload test" > "$tmpfile"
|
||||
put_output=$(s3 s3 cp "$tmpfile" "s3://$bucket_name/s3-uploaded.txt")
|
||||
put_ok=$(echo "$put_output" | grep -q "upload:" && echo "true" || echo "false")
|
||||
check "PutObject upload" "true" "$put_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 4. S3 ListObjectsV2
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 ListObjectsV2 ---"
|
||||
list_objects=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json)
|
||||
object_found=$(echo "$list_objects" | \
|
||||
jq_r '[.Contents[]? | .Key] | if any(. == "s3-uploaded.txt") then "true" else "false" end')
|
||||
check "Object found in ListObjectsV2" "true" "$object_found"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 5. S3 HeadObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 HeadObject ---"
|
||||
head_output=$(s3 s3api head-object --bucket "$bucket_name" --key "s3-uploaded.txt" --output json)
|
||||
head_size=$(echo "$head_output" | jq_r '.ContentLength // 0')
|
||||
original_size=$(wc -c < "$tmpfile" | tr -d ' ')
|
||||
check "HeadObject returns correct size" "$original_size" "$head_size"
|
||||
rm -f "$tmpfile"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 6. S3 GetObject (download) + content verify
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 GetObject ---"
|
||||
download_file=$(mktemp); cleanup_files="$cleanup_files $download_file"
|
||||
s3 s3 cp "s3://$bucket_name/s3-uploaded.txt" "$download_file" >/dev/null
|
||||
downloaded_content=$(cat "$download_file")
|
||||
check "GetObject content matches" "hello from s3 upload test" "$downloaded_content"
|
||||
rm -f "$download_file"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 7. S3 CopyObject (server-side copy)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 CopyObject ---"
|
||||
copy_output=$(s3 s3 cp "s3://$bucket_name/s3-uploaded.txt" "s3://$bucket_name/s3-copied.txt")
|
||||
copy_ok=$(echo "$copy_output" | grep -q "copy:" && echo "true" || echo "false")
|
||||
check "CopyObject" "true" "$copy_ok"
|
||||
|
||||
# Verify copied content
|
||||
copy_download=$(mktemp); cleanup_files="$cleanup_files $copy_download"
|
||||
s3 s3 cp "s3://$bucket_name/s3-copied.txt" "$copy_download" >/dev/null
|
||||
check "Copied object content matches" "hello from s3 upload test" "$(cat "$copy_download")"
|
||||
rm -f "$copy_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 8. S3 DeleteObject
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 DeleteObject ---"
|
||||
s3 s3 rm "s3://$bucket_name/s3-copied.txt" >/dev/null
|
||||
# Verify object is gone
|
||||
list_after_delete=$(s3 s3api list-objects-v2 --bucket "$bucket_name" --output json)
|
||||
copied_gone=$(echo "$list_after_delete" | \
|
||||
jq_r '[.Contents[]? | .Key] | if any(. == "s3-copied.txt") then "false" else "true" end')
|
||||
check "Deleted object no longer listed" "true" "$copied_gone"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 9. Multipart upload (>5MB triggers multipart)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- S3 multipart upload (7MB) ---"
|
||||
large_file=$(mktemp); cleanup_files="$cleanup_files $large_file"
|
||||
dd if=/dev/urandom of="$large_file" bs=1048576 count=7 2>/dev/null
|
||||
large_size=$(wc -c < "$large_file" | tr -d ' ')
|
||||
large_put=$(s3 s3 cp "$large_file" "s3://$bucket_name/large-file.bin")
|
||||
large_ok=$(echo "$large_put" | grep -q "upload:" && echo "true" || echo "false")
|
||||
check "Multipart upload (7MB)" "true" "$large_ok"
|
||||
|
||||
# Verify size via HeadObject
|
||||
large_head=$(s3 s3api head-object --bucket "$bucket_name" --key "large-file.bin" --output json)
|
||||
remote_size=$(echo "$large_head" | jq_r '.ContentLength // 0')
|
||||
check "Multipart upload size matches ($large_size bytes)" "$large_size" "$remote_size"
|
||||
|
||||
# Download and verify size
|
||||
large_download=$(mktemp); cleanup_files="$cleanup_files $large_download"
|
||||
s3 s3 cp "s3://$bucket_name/large-file.bin" "$large_download" >/dev/null
|
||||
download_size=$(wc -c < "$large_download" | tr -d ' ')
|
||||
check "Multipart download size matches" "$large_size" "$download_size"
|
||||
rm -f "$large_file" "$large_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 10. Range request (partial GetObject)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Range request ---"
|
||||
range_file=$(mktemp); cleanup_files="$cleanup_files $range_file"
|
||||
echo "hello range test content" > "$range_file"
|
||||
s3 s3 cp "$range_file" "s3://$bucket_name/range-test.txt" >/dev/null
|
||||
rm -f "$range_file"
|
||||
|
||||
range_download=$(mktemp); cleanup_files="$cleanup_files $range_download"
|
||||
s3 s3api get-object --bucket "$bucket_name" --key "range-test.txt" \
|
||||
--range "bytes=0-4" "$range_download" --output json >/dev/null
|
||||
range_content=$(cat "$range_download")
|
||||
check "Range request returns partial content" "hello" "$range_content"
|
||||
rm -f "$range_download"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 11. Presigned URLs
|
||||
# ---------------------------------------------
|
||||
# Storage supports S3 presigned URLs (query-parameter auth).
|
||||
|
||||
echo ""
|
||||
echo "--- Presigned URLs ---"
|
||||
presign_file=$(mktemp); cleanup_files="$cleanup_files $presign_file"
|
||||
echo "presigned content test" > "$presign_file"
|
||||
s3 s3 cp "$presign_file" "s3://$bucket_name/presign-test.txt" >/dev/null
|
||||
rm -f "$presign_file"
|
||||
|
||||
presigned_url=$(s3 s3 presign "s3://$bucket_name/presign-test.txt")
|
||||
presign_body=$(curl -s "$presigned_url" || true)
|
||||
check "Presigned URL returns correct content" "presigned content test" "$presign_body"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 12. Authentication
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Authentication ---"
|
||||
bad_output=$(AWS_ACCESS_KEY_ID="invalid-key" \
|
||||
AWS_SECRET_ACCESS_KEY="invalid-secret" \
|
||||
aws s3api list-buckets \
|
||||
--endpoint-url "$S3_ENDPOINT" \
|
||||
--region "$REGION" \
|
||||
--output json 2>&1 || true)
|
||||
bad_ok=$(echo "$bad_output" | grep -qi "denied\|invalid\|error\|403\|401" && echo "true" || echo "false")
|
||||
check "Invalid credentials rejected" "true" "$bad_ok"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 13. Cleanup
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Cleanup ---"
|
||||
s3 s3 rm "s3://$bucket_name/" --recursive >/dev/null
|
||||
s3 s3api delete-bucket --bucket "$bucket_name" >/dev/null
|
||||
# Verify bucket is gone
|
||||
bucket_gone=$(s3 s3api list-buckets --output json | \
|
||||
jq_r --arg name "$bucket_name" '[.Buckets[] | .Name] | if any(. == $name) then "false" else "true" end')
|
||||
check "Bucket deleted via S3" "true" "$bucket_gone"
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,543 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Smoke test for self-hosted Supabase - verifies core functionality end-to-end.
|
||||
#
|
||||
# Usage:
|
||||
# sh test-self-hosted.sh # Uses http://localhost:8000
|
||||
# sh test-self-hosted.sh <base_url> # Custom URL
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Running self-hosted Supabase instance
|
||||
# - .env file with keys configured
|
||||
# - jq (for JSON parsing)
|
||||
# - sha256sum or shasum (for file integrity checks)
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cleanup_files=""
|
||||
trap 'rm -f $cleanup_files' EXIT
|
||||
|
||||
BASE_URL="${1:-http://localhost:8000}"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run from the project directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Error: jq not found. Install it: https://jqlang.github.io/jq/download/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Portable file hash: prefers sha256sum (Linux), falls back to shasum (macOS)
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
file_hash() { sha256sum "$1" | awk '{print $1}'; }
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
file_hash() { shasum -a 256 "$1" | awk '{print $1}'; }
|
||||
else
|
||||
echo "Error: sha256sum or shasum not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read keys from .env
|
||||
ANON_KEY=$(grep '^ANON_KEY=' .env | cut -d= -f2-)
|
||||
SERVICE_ROLE_KEY=$(grep '^SERVICE_ROLE_KEY=' .env | cut -d= -f2-)
|
||||
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' .env | cut -d= -f2-)
|
||||
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' .env | cut -d= -f2-)
|
||||
DASHBOARD_USERNAME=$(grep '^DASHBOARD_USERNAME=' .env | cut -d= -f2-)
|
||||
DASHBOARD_PASSWORD=$(grep '^DASHBOARD_PASSWORD=' .env | cut -d= -f2-)
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
check() {
|
||||
test_name="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
echo " PASS: $test_name"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo " FAIL: $test_name (expected $expected, got $actual)"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
http_status() {
|
||||
url="$1"
|
||||
shift
|
||||
curl -s -o /dev/null -w "%{http_code}" "$@" "$url"
|
||||
}
|
||||
|
||||
http_body() {
|
||||
url="$1"
|
||||
shift
|
||||
curl -s "$@" "$url"
|
||||
}
|
||||
|
||||
# Is a compose service running? Falls back to a label lookup so it works
|
||||
# regardless of which override files are loaded in this shell.
|
||||
service_running() {
|
||||
svc="$1"
|
||||
docker compose ps --services --status running 2>/dev/null | grep -qx "$svc" && return 0
|
||||
docker ps --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME:-supabase}" \
|
||||
--filter "label=com.docker.compose.service=$svc" \
|
||||
--filter "status=running" --quiet | grep -q '.'
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Self-hosted smoke test against $BASE_URL ==="
|
||||
echo ""
|
||||
|
||||
# ---------------------------------------------
|
||||
# 1. Container health (via docker compose)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo "--- Container health ---"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
container_status=$(docker compose ps --format json 2>/dev/null | jq -rs '
|
||||
[.[] | select(.State != "running" or (.Health != "" and .Health != "healthy"))]
|
||||
| (length | tostring) + "|" + ([.[] | .Service + ": State=" + .State + " Health=" + (.Health // "none")] | join(", "))
|
||||
' 2>/dev/null || echo "?|")
|
||||
unhealthy="${container_status%%|*}"
|
||||
container_issues="${container_status#*|}"
|
||||
if [ "$unhealthy" = "0" ]; then
|
||||
check "All containers healthy" "0" "$unhealthy"
|
||||
elif [ "$unhealthy" = "?" ]; then
|
||||
echo " SKIP: Could not check container health"
|
||||
else
|
||||
check "All containers healthy ($container_issues)" "0" "$unhealthy"
|
||||
fi
|
||||
else
|
||||
echo " SKIP: docker not available"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 2. Studio dashboard
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Studio dashboard ---"
|
||||
# Studio may redirect (307/302) after auth - follow redirects
|
||||
check "Studio accessible with basic auth" "200" \
|
||||
"$(http_status "$BASE_URL/" -L -u "$DASHBOARD_USERNAME:$DASHBOARD_PASSWORD")"
|
||||
check "Studio rejects without auth" "401" \
|
||||
"$(http_status "$BASE_URL/")"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 3. Auth: create user, sign in, get user, public signup, delete
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Auth: user lifecycle ---"
|
||||
|
||||
test_email="smoke-test-$$@example.com"
|
||||
test_password="smoke-test-password-123456"
|
||||
|
||||
# Create user via admin API (works regardless of email autoconfirm setting)
|
||||
create_resp=$(http_body "$BASE_URL/auth/v1/admin/users" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\",\"email_confirm\":true}")
|
||||
|
||||
user_id=$(echo "$create_resp" | jq -r '.id // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$user_id" ]; then
|
||||
check "Create user (admin)" "true" "true"
|
||||
|
||||
# Sign in via public endpoint
|
||||
signin_resp=$(http_body "$BASE_URL/auth/v1/token?grant_type=password" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$test_email\",\"password\":\"$test_password\"}")
|
||||
|
||||
access_token=$(echo "$signin_resp" | jq -r '.access_token // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$access_token" ]; then
|
||||
check "Sign in user" "true" "true"
|
||||
|
||||
# Get user profile with session JWT
|
||||
check "Get user profile" "200" \
|
||||
"$(http_status "$BASE_URL/auth/v1/user" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Authorization: Bearer $access_token")"
|
||||
else
|
||||
check "Sign in user" "true" "false"
|
||||
fi
|
||||
|
||||
# Delete user
|
||||
delete_status=$(http_status "$BASE_URL/auth/v1/admin/users/$user_id" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
|
||||
check "Delete user (admin)" "200" "$delete_status"
|
||||
else
|
||||
check "Create user (admin)" "true" "false"
|
||||
fi
|
||||
|
||||
# Public signup (optional - depends on email autoconfirm setting)
|
||||
signup_email="smoke-signup-$$@example.com"
|
||||
signup_resp=$(http_body "$BASE_URL/auth/v1/signup" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"email\":\"$signup_email\",\"password\":\"$test_password\"}")
|
||||
|
||||
signup_token=$(echo "$signup_resp" | jq -r '.access_token // empty' 2>/dev/null)
|
||||
signup_user_id=$(echo "$signup_resp" | jq -r '.id // .user.id // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$signup_token" ]; then
|
||||
check "Public signup (autoconfirm on)" "true" "true"
|
||||
else
|
||||
echo " SKIP: Public signup (autoconfirm is off)"
|
||||
fi
|
||||
|
||||
# Clean up signup user if created
|
||||
if [ -n "$signup_user_id" ]; then
|
||||
http_status "$BASE_URL/auth/v1/admin/users/$signup_user_id" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 4. PostgREST: query
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- PostgREST ---"
|
||||
check "REST API route with anon key" "403" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
|
||||
echo ""
|
||||
echo "--- PostgREST ---"
|
||||
check "REST API route with service role key" "200" \
|
||||
"$(http_status "$BASE_URL/rest/v1/" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY")"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 5. GraphQL
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- GraphQL (optional; off by default) ---"
|
||||
# pg_graphql is OFF by default since the PG17 image (the image drops the extension
|
||||
# on init, matching platform behavior for new projects), but users may enable it
|
||||
# (Studio extensions UI / CREATE EXTENSION pg_graphql). Both are valid states. A
|
||||
# healthy endpoint returns HTTP 200 either way:
|
||||
# enabled => {"data": ...}
|
||||
# disabled => {"errors":[{"message":"pg_graphql extension is not enabled."}]}
|
||||
# Assert the status AND the response shape, so a non-200, non-JSON, or empty body
|
||||
# (a real gateway/runtime failure) is not silently classified as "disabled".
|
||||
gql_status=$(http_status "$BASE_URL/graphql/v1" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')
|
||||
gql_body=$(http_body "$BASE_URL/graphql/v1" \
|
||||
-H "apikey: $ANON_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ __typename }"}')
|
||||
if [ "$gql_status" = "200" ] && echo "$gql_body" | jq -e '.data' >/dev/null 2>&1; then
|
||||
gql_state="enabled"
|
||||
elif [ "$gql_status" = "200" ] && echo "$gql_body" | jq -e '.errors' >/dev/null 2>&1; then
|
||||
gql_state="disabled"
|
||||
else
|
||||
gql_state="unhealthy (HTTP $gql_status)"
|
||||
fi
|
||||
case "$gql_state" in
|
||||
enabled | disabled) gql_health="healthy" ;;
|
||||
*) gql_health="unhealthy" ;;
|
||||
esac
|
||||
check "GraphQL endpoint healthy" "healthy" "$gql_health"
|
||||
echo " (GraphQL is $gql_state)"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 6. Storage: create bucket, upload >6MB file, download, cleanup
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Storage: bucket + file lifecycle ---"
|
||||
|
||||
bucket_name="smoke-test-$$"
|
||||
|
||||
# Create bucket
|
||||
create_bucket_status=$(http_status "$BASE_URL/storage/v1/bucket" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"id\":\"$bucket_name\",\"name\":\"$bucket_name\",\"public\":true}")
|
||||
check "Create bucket" "200" "$create_bucket_status"
|
||||
|
||||
if [ "$create_bucket_status" = "200" ]; then
|
||||
# Generate a ~7MB file
|
||||
tmpfile=$(mktemp); cleanup_files="$cleanup_files $tmpfile"
|
||||
dd if=/dev/urandom of="$tmpfile" bs=1048576 count=7 2>/dev/null
|
||||
|
||||
# Upload file
|
||||
upload_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/test-large-file.bin" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@$tmpfile")
|
||||
check "Upload 7MB file" "200" "$upload_status"
|
||||
|
||||
# Download file and verify integrity
|
||||
download_tmp=$(mktemp); cleanup_files="$cleanup_files $download_tmp"
|
||||
curl -s "$BASE_URL/storage/v1/object/public/$bucket_name/test-large-file.bin" -o "$download_tmp"
|
||||
original_size=$(wc -c < "$tmpfile" | tr -d ' ')
|
||||
download_size=$(wc -c < "$download_tmp" | tr -d ' ')
|
||||
check "Download file (size matches)" "$original_size" "$download_size"
|
||||
original_hash=$(file_hash "$tmpfile")
|
||||
download_hash=$(file_hash "$download_tmp")
|
||||
check "Download file (hash matches)" "$original_hash" "$download_hash"
|
||||
rm -f "$download_tmp"
|
||||
|
||||
rm -f "$tmpfile"
|
||||
|
||||
# Signed URL: upload a small file, create signed URL, fetch without auth
|
||||
sign_upload_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/sign-test.txt" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: text/plain" \
|
||||
--data-binary "signed url test content")
|
||||
check "Upload file for signing" "200" "$sign_upload_status"
|
||||
|
||||
if [ "$sign_upload_status" = "200" ]; then
|
||||
sign_resp=$(http_body "$BASE_URL/storage/v1/object/sign/$bucket_name/sign-test.txt" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"expiresIn": 600}')
|
||||
signed_path=$(echo "$sign_resp" | jq -r '.signedURL // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$signed_path" ]; then
|
||||
check "Create signed URL" "true" "true"
|
||||
# Fetch signed URL without any auth headers (goes through the API gateway)
|
||||
signed_content=$(curl -s "$BASE_URL/storage/v1$signed_path")
|
||||
check "Fetch signed URL (no auth)" "signed url test content" "$signed_content"
|
||||
else
|
||||
check "Create signed URL" "true" "false"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Delete file
|
||||
delete_file_status=$(http_status "$BASE_URL/storage/v1/object/$bucket_name/test-large-file.bin" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
|
||||
check "Delete file" "200" "$delete_file_status"
|
||||
|
||||
# Delete signed test file
|
||||
http_status "$BASE_URL/storage/v1/object/$bucket_name/sign-test.txt" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" >/dev/null 2>&1
|
||||
|
||||
# Delete bucket
|
||||
delete_bucket_status=$(http_status "$BASE_URL/storage/v1/bucket/$bucket_name" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
|
||||
check "Delete bucket" "200" "$delete_bucket_status"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 6b. Storage: TUS resumable upload
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Storage: TUS resumable upload ---"
|
||||
|
||||
tus_bucket="smoke-tus-$$"
|
||||
|
||||
tus_bucket_status=$(http_status "$BASE_URL/storage/v1/bucket" \
|
||||
-X POST \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"id\":\"$tus_bucket\",\"name\":\"$tus_bucket\",\"public\":true}")
|
||||
check "TUS: create bucket" "200" "$tus_bucket_status"
|
||||
|
||||
if [ "$tus_bucket_status" = "200" ]; then
|
||||
# Generate a ~7MB file (above Studio's 6MB TUS threshold)
|
||||
tusfile=$(mktemp); cleanup_files="$cleanup_files $tusfile"
|
||||
dd if=/dev/urandom of="$tusfile" bs=1048576 count=7 2>/dev/null
|
||||
tus_file_size=$(wc -c < "$tusfile" | tr -d ' ')
|
||||
tus_chunk_size=$((4 * 1048576)) # 4MB first chunk
|
||||
|
||||
# Encode TUS metadata values as base64
|
||||
tus_bucket_b64=$(printf '%s' "$tus_bucket" | base64)
|
||||
tus_object_b64=$(printf '%s' "tus-test-file.bin" | base64)
|
||||
tus_mime_b64=$(printf '%s' "application/octet-stream" | base64)
|
||||
|
||||
# 1. Create resumable upload
|
||||
tus_create_resp=$(curl -s -i -X POST \
|
||||
"$BASE_URL/storage/v1/upload/resumable" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Tus-Resumable: 1.0.0" \
|
||||
-H "Upload-Length: $tus_file_size" \
|
||||
-H "Upload-Metadata: bucketName $tus_bucket_b64,objectName $tus_object_b64,contentType $tus_mime_b64" \
|
||||
-H "x-upsert: true")
|
||||
tus_create_status=$(echo "$tus_create_resp" | grep -m1 '^HTTP/' | grep -o '[0-9][0-9][0-9]')
|
||||
# Supabase Storage always returns an absolute Location URL (see generateUrl in storage/src/http/routes/tus/lifecycle.ts)
|
||||
tus_location=$(echo "$tus_create_resp" | grep -i '^location:' | tr -d '\r' | sed 's/^[Ll]ocation: *//')
|
||||
check "TUS: create resumable upload" "201" "$tus_create_status"
|
||||
|
||||
if [ -n "$tus_location" ]; then
|
||||
# 2. Upload first chunk (0 to 4MB)
|
||||
tus_chunk1_status=$(dd if="$tusfile" bs=1048576 count=4 2>/dev/null | \
|
||||
curl -s -o /dev/null -w "%{http_code}" -X PATCH \
|
||||
"$tus_location" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Tus-Resumable: 1.0.0" \
|
||||
-H "Upload-Offset: 0" \
|
||||
-H "Content-Type: application/offset+octet-stream" \
|
||||
--data-binary @-)
|
||||
check "TUS: upload chunk 1 (4MB)" "204" "$tus_chunk1_status"
|
||||
|
||||
# 3. Upload second chunk (4MB to end)
|
||||
tus_remaining=$((tus_file_size - tus_chunk_size))
|
||||
tus_chunk2_status=$(dd if="$tusfile" bs=1048576 skip=4 count=3 2>/dev/null | \
|
||||
curl -s -o /dev/null -w "%{http_code}" -X PATCH \
|
||||
"$tus_location" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" \
|
||||
-H "Tus-Resumable: 1.0.0" \
|
||||
-H "Upload-Offset: $tus_chunk_size" \
|
||||
-H "Content-Type: application/offset+octet-stream" \
|
||||
--data-binary @-)
|
||||
check "TUS: upload chunk 2 (remaining)" "204" "$tus_chunk2_status"
|
||||
|
||||
# 4. Verify download matches original (hash check proves correct chunk reassembly)
|
||||
tus_download_tmp=$(mktemp); cleanup_files="$cleanup_files $tus_download_tmp"
|
||||
curl -s "$BASE_URL/storage/v1/object/public/$tus_bucket/tus-test-file.bin" -o "$tus_download_tmp"
|
||||
tus_download_size=$(wc -c < "$tus_download_tmp" | tr -d ' ')
|
||||
check "TUS: download size matches" "$tus_file_size" "$tus_download_size"
|
||||
tus_original_hash=$(file_hash "$tusfile")
|
||||
tus_download_hash=$(file_hash "$tus_download_tmp")
|
||||
check "TUS: download hash matches" "$tus_original_hash" "$tus_download_hash"
|
||||
rm -f "$tus_download_tmp"
|
||||
fi
|
||||
|
||||
rm -f "$tusfile"
|
||||
|
||||
# Cleanup: delete file and bucket
|
||||
http_status "$BASE_URL/storage/v1/object/$tus_bucket/tus-test-file.bin" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY" >/dev/null 2>&1
|
||||
|
||||
tus_delete_bucket=$(http_status "$BASE_URL/storage/v1/bucket/$tus_bucket" \
|
||||
-X DELETE \
|
||||
-H "apikey: $SERVICE_ROLE_KEY" \
|
||||
-H "Authorization: Bearer $SERVICE_ROLE_KEY")
|
||||
check "TUS: delete bucket" "200" "$tus_delete_bucket"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# 7. Edge Functions
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Edge Functions ---"
|
||||
fn_resp=$(http_body "$BASE_URL/functions/v1/hello" \
|
||||
-X POST \
|
||||
-H "apikey: $SUPABASE_PUBLISHABLE_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}')
|
||||
check "Call hello function" '{"message":"Hello from Edge Functions!"}' "$fn_resp"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 8. pg-meta (Studio backend)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- pg-meta ---"
|
||||
check "pg-meta with service_role key" "200" \
|
||||
"$(http_status "$BASE_URL/pg/schemas" \
|
||||
-H "apikey: $SERVICE_ROLE_KEY")"
|
||||
check "pg-meta rejects anon key" "403" \
|
||||
"$(http_status "$BASE_URL/pg/schemas" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
check "pg-meta rejects no key" "401" \
|
||||
"$(http_status "$BASE_URL/pg/schemas")"
|
||||
|
||||
echo ""
|
||||
echo "--- MCP (blocked by default) ---"
|
||||
check "/api/mcp blocked" "403" \
|
||||
"$(http_status "$BASE_URL/api/mcp")"
|
||||
check "/mcp blocked" "403" \
|
||||
"$(http_status "$BASE_URL/mcp")"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 9. Realtime
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Realtime ---"
|
||||
check "Realtime health (ping)" "200" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/ping" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
|
||||
# Management endpoints must be blocked at the gateway (even with a valid key)
|
||||
check "Realtime /api/tenants blocked" "403" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/tenants" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
check "Realtime /api/openapi blocked" "403" \
|
||||
"$(http_status "$BASE_URL/realtime/v1/api/openapi" \
|
||||
-H "apikey: $ANON_KEY")"
|
||||
|
||||
# ---------------------------------------------
|
||||
# 10. Database pooler (transaction mode)
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "--- Database pooler (transaction mode) ---"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
pg_password=$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2-)
|
||||
pooler_tenant_id=$(grep '^POOLER_TENANT_ID=' .env | cut -d= -f2-)
|
||||
# Connect as the postgres role through whichever transaction pooler is running:
|
||||
# Supavisor (default) -> service 'supavisor', port 6543, user 'postgres.<tenant>'
|
||||
# PgBouncer (override) -> service 'pgbouncer', port 6432 (published as 6543), user 'postgres'
|
||||
# psql runs inside the db container (always has a client) and reaches the
|
||||
# pooler over the compose network.
|
||||
if service_running supavisor; then
|
||||
pooler_user="postgres.$pooler_tenant_id"; pooler_host="supavisor"; pooler_port=6543
|
||||
elif service_running pgbouncer; then
|
||||
pooler_user="postgres"; pooler_host="pgbouncer"; pooler_port=6432
|
||||
else
|
||||
pooler_user=""
|
||||
fi
|
||||
if [ -n "$pooler_user" ]; then
|
||||
pooler_result=$(docker exec -e PGPASSWORD="$pg_password" supabase-db \
|
||||
psql "host=$pooler_host port=$pooler_port user=$pooler_user dbname=postgres sslmode=disable" \
|
||||
-tAc "select 'pooler_ok';" 2>/dev/null | tr -d '[:space:]')
|
||||
check "Pooler transaction-mode query (as postgres)" "pooler_ok" "$pooler_result"
|
||||
else
|
||||
check "Pooler running (supavisor or pgbouncer)" "true" "false"
|
||||
fi
|
||||
else
|
||||
echo " SKIP: docker not available"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $pass passed, $fail failed ==="
|
||||
echo ""
|
||||
|
||||
if [ "$fail" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,380 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Hermetic test for update.sh (the self-hosted in-place update script).
|
||||
#
|
||||
# Builds a tiny synthetic "upstream" git repo with two tagged releases
|
||||
# (self-hosted/v0.9.0 -> self-hosted/v1.1.0; 1.0.0 exists only as a manifest key),
|
||||
# where the target ships a breaking-change manifest with entries at the base and
|
||||
# inside the window. It then simulates a configured deployment based on v0.9.0
|
||||
# and runs update.sh via a SUPABASE_REPO_URL override (no network), asserting:
|
||||
# the 3-way merge preserves secrets/data/overrides, adds new .env keys (but not
|
||||
# ones the user commented out), applies clean merges, reports real conflicts,
|
||||
# honors .gitignore for user-owned paths, surfaces only the in-window gate
|
||||
# entries (base-version entry excluded), and advances the stamp ONLY on a clean
|
||||
# apply. Also covers the clean-apply path, default-target resolution, --from,
|
||||
# --dry-run, the missing-stamp report-only path, and a malformed manifest being
|
||||
# refused.
|
||||
#
|
||||
# Usage:
|
||||
# sh tests/test-update.sh # run from the docker/ directory
|
||||
#
|
||||
|
||||
set -eu
|
||||
|
||||
# Isolate from the developer's global/system git config (gpgsign, hooksPath,
|
||||
# templateDir, core.excludesfile) so neither the synthetic commits nor update.sh's
|
||||
# internal git calls (fetch, merge-file, check-ignore) are affected. Exported so
|
||||
# the update.sh subprocess inherits them too.
|
||||
export GIT_CONFIG_GLOBAL=/dev/null
|
||||
export GIT_CONFIG_NOSYSTEM=1
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
DOCKER_DIR=$(dirname "$SCRIPT_DIR")
|
||||
UPDATE_SH="$DOCKER_DIR/update.sh"
|
||||
|
||||
[ -f "$UPDATE_SH" ] || { echo "ERROR: $UPDATE_SH not found"; exit 1; }
|
||||
|
||||
WORK=$(mktemp -d)
|
||||
cleanup() { rm -rf "$WORK"; }
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
ok() { PASS=$((PASS+1)); printf " ok - %s\n" "$1"; }
|
||||
bad() { FAIL=$((FAIL+1)); printf " FAIL - %s\n" "$1"; }
|
||||
|
||||
assert_file_contains() { # <file> <pattern> <desc>
|
||||
if grep -qF "$2" "$1" 2>/dev/null; then ok "$3"; else bad "$3 (missing '$2' in $1)"; fi
|
||||
}
|
||||
assert_file_missing_pattern() { # <file> <pattern> <desc>
|
||||
if grep -qF "$2" "$1" 2>/dev/null; then bad "$3 (unexpected '$2' in $1)"; else ok "$3"; fi
|
||||
}
|
||||
assert_line() { # <file> <ERE> <desc>
|
||||
if grep -qE "$2" "$1" 2>/dev/null; then ok "$3"; else bad "$3 (no line matching /$2/ in $1)"; fi
|
||||
}
|
||||
assert_no_line() { # <file> <ERE> <desc>
|
||||
if grep -qE "$2" "$1" 2>/dev/null; then bad "$3 (unexpected line matching /$2/ in $1)"; else ok "$3"; fi
|
||||
}
|
||||
assert_path_exists() { # <path> <desc>
|
||||
if [ -e "$1" ]; then ok "$2"; else bad "$2 ($1 missing)"; fi
|
||||
}
|
||||
assert_path_absent() { # <path> <desc>
|
||||
if [ -e "$1" ]; then bad "$2 ($1 should not exist)"; else ok "$2"; fi
|
||||
}
|
||||
|
||||
# portable in-place sed (BSD + GNU): sedi <expr> <file>
|
||||
sedi() { sed "$1" "$2" > "$2.tmp" && mv "$2.tmp" "$2"; }
|
||||
|
||||
# --- 1. Build the synthetic upstream repo -----------------------------------
|
||||
|
||||
SRC="$WORK/upstream"
|
||||
mkdir -p "$SRC/docker/volumes/api"
|
||||
cd "$SRC"
|
||||
git init -q
|
||||
git config user.email t@t.t
|
||||
git config user.name t
|
||||
|
||||
cat > docker/docker-compose.yml <<'EOF'
|
||||
services:
|
||||
studio:
|
||||
image: supabase/studio:OLD
|
||||
db:
|
||||
image: supabase/postgres:15
|
||||
EOF
|
||||
cat > docker/.env.example <<'EOF'
|
||||
POSTGRES_PASSWORD=changeme
|
||||
JWT_SECRET=changeme
|
||||
KEEP_ME=base-default
|
||||
EOF
|
||||
printf 'base kong\n' > docker/volumes/api/kong.yml
|
||||
printf 'remove me\n' > docker/old-only.txt
|
||||
cp "$DOCKER_DIR/.gitignore" docker/.gitignore
|
||||
mkdir -p docker/volumes/functions/main
|
||||
printf 'base main\n' > docker/volumes/functions/main/index.ts
|
||||
git add -A && git commit -qm base && git tag self-hosted/v0.9.0
|
||||
|
||||
# target commit
|
||||
cat > docker/docker-compose.yml <<'EOF'
|
||||
services:
|
||||
studio:
|
||||
image: supabase/studio:NEW
|
||||
db:
|
||||
image: supabase/postgres:17
|
||||
EOF
|
||||
cat > docker/.env.example <<'EOF'
|
||||
POSTGRES_PASSWORD=changeme
|
||||
JWT_SECRET=changeme
|
||||
KEEP_ME=base-default
|
||||
NEW_KEY=new-default
|
||||
EOF
|
||||
printf 'brand new\n' > docker/new-only.txt
|
||||
printf 'target main\n' > docker/volumes/functions/main/index.ts
|
||||
mkdir -p docker/volumes/functions/hello
|
||||
printf 'target hello\n' > docker/volumes/functions/hello/index.ts
|
||||
# A gitignored sample under volumes/snippets shipped by upstream: must NOT
|
||||
# overwrite the user's file of the same name (exercises is_excluded directly).
|
||||
mkdir -p docker/volumes/snippets
|
||||
printf 'VENDOR SEED\n' > docker/volumes/snippets/seed.sql
|
||||
# Manifest with entries at/below and inside the window:
|
||||
# 0.9.0 == the base -> must be EXCLUDED (window is half-open: (base, target]).
|
||||
# 1.0.0 inside -> surfaces (carries requires+gate).
|
||||
# 1.1.0 == target -> surfaces; requires-less and sorts LAST (no _schema after),
|
||||
# guarding the set -e regression where a requires-less final entry
|
||||
# aborted the script. Do NOT add a version key after 1.1.0.
|
||||
cat > docker/upgrades.json <<'EOF'
|
||||
{
|
||||
"0.9.0": {
|
||||
"breaking": true
|
||||
},
|
||||
"1.0.0": {
|
||||
"breaking": true,
|
||||
"gate": "utils/demo-migrate.sh",
|
||||
"migration_guide_url": "https://example.test/guide",
|
||||
"requires": ["Run the demo migration first."]
|
||||
},
|
||||
"1.1.0": {
|
||||
"breaking": true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
git rm -q docker/old-only.txt
|
||||
git add -A
|
||||
git add -f docker/volumes/functions/hello/index.ts docker/volumes/snippets/seed.sql
|
||||
# Release tag for the target / default-target (latest self-hosted/v*) path.
|
||||
git commit -qm target && git tag self-hosted/v1.1.0
|
||||
|
||||
# A ref whose upgrades.json is valid JSON but NOT an object. update.sh must
|
||||
# refuse (die) rather than silently skip the gate. Named so latest_release_tag
|
||||
# ignores it (not self-hosted/v*), leaving the default-target path on v1.1.0.
|
||||
printf '["valid JSON, but not an object"]\n' > docker/upgrades.json
|
||||
git add -A && git commit -qm 'malformed manifest' && git tag malformed-manifest
|
||||
|
||||
# --- helper: lay down a deployment based on v0.9.0 --------------------------
|
||||
# make_deploy <dir> [conflict] - pass "conflict" to pin the studio image so the
|
||||
# merge produces a real conflict; omit for a clean apply.
|
||||
|
||||
make_deploy() { # <dir> [conflict]
|
||||
d="$1"
|
||||
_mode="${2:-clean}"
|
||||
mkdir -p "$d"
|
||||
git -C "$SRC" archive self-hosted/v0.9.0 docker | tar -x -C "$d" --strip-components=1
|
||||
cp "$UPDATE_SH" "$d/update.sh"
|
||||
# configured .env: real secret, an extra user key, and KEEP_ME commented out
|
||||
# on purpose (must NOT be re-added by the .env key-union).
|
||||
cp "$d/.env.example" "$d/.env"
|
||||
sedi "s/^POSTGRES_PASSWORD=.*/POSTGRES_PASSWORD=test-secret-123/" "$d/.env"
|
||||
sedi "s/^KEEP_ME=/#KEEP_ME=/" "$d/.env"
|
||||
printf 'EXTRA_USER_KEY=mine\n' >> "$d/.env"
|
||||
# user-owned override (must never be touched)
|
||||
printf 'services: {}\n# my override\n' > "$d/docker-compose.override.yml"
|
||||
# data dirs with sentinels (must never be touched)
|
||||
mkdir -p "$d/volumes/db/data" "$d/volumes/storage"
|
||||
printf 'DBDATA\n' > "$d/volumes/db/data/keep.txt"
|
||||
printf 'OBJ\n' > "$d/volumes/storage/keep.txt"
|
||||
# user adds a line to kong (upstream unchanged -> clean merge, must survive)
|
||||
printf 'user added line\n' >> "$d/volumes/api/kong.yml"
|
||||
# user-owned paths per .gitignore (must not be touched by the merge)
|
||||
mkdir -p "$d/volumes/snippets" "$d/volumes/functions/my-fn"
|
||||
printf 'USER_SNIPPET\n' > "$d/volumes/snippets/user.sql"
|
||||
printf 'user fn\n' > "$d/volumes/functions/my-fn/index.ts"
|
||||
# user file at the SAME path as the vendor snippet shipped at target:
|
||||
# is_excluded must skip it so the user's content survives.
|
||||
printf 'USER SEED\n' > "$d/volumes/snippets/seed.sql"
|
||||
# legacy sample fn in snapshot at target but gitignored - must not overwrite
|
||||
mkdir -p "$d/volumes/functions/hello"
|
||||
printf 'user hello\n' > "$d/volumes/functions/hello/index.ts"
|
||||
# version stamp pointing at the base (ref only; update.sh derives the rest)
|
||||
printf 'ref=self-hosted/v0.9.0\n' > "$d/.supabase-version"
|
||||
if [ "$_mode" = "conflict" ]; then
|
||||
# user pins the studio image (same line upstream changes -> conflict)
|
||||
sedi "s#supabase/studio:OLD#supabase/studio:USER-PINNED#" "$d/docker-compose.yml"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== update.sh: apply path (with a conflict) ==="
|
||||
|
||||
DEPLOY="$WORK/deploy"
|
||||
make_deploy "$DEPLOY" conflict
|
||||
cd "$DEPLOY"
|
||||
rc=0
|
||||
SUPABASE_REPO_URL="$SRC" sh ./update.sh --to self-hosted/v1.1.0 --yes > "$WORK/apply.log" 2>&1 || rc=$?
|
||||
|
||||
sed 's/^/ | /' "$WORK/apply.log"
|
||||
|
||||
if [ "$rc" = "2" ]; then ok "exit status 2 signals conflicts"; else bad "expected exit 2 (conflicts), got $rc"; fi
|
||||
assert_file_contains ".env" "POSTGRES_PASSWORD=test-secret-123" "user secret preserved"
|
||||
assert_file_contains ".env" "NEW_KEY=new-default" "new .env key appended"
|
||||
assert_file_contains ".env" "EXTRA_USER_KEY=mine" "extra user key kept"
|
||||
assert_line ".env" "^#KEEP_ME=base-default" "user's commented key left commented"
|
||||
assert_no_line ".env" "^KEEP_ME=" "commented .env key not re-added uncommented"
|
||||
assert_file_contains "docker-compose.override.yml" "my override" "override untouched"
|
||||
assert_file_contains "volumes/db/data/keep.txt" "DBDATA" "db data untouched"
|
||||
assert_file_contains "volumes/storage/keep.txt" "OBJ" "storage untouched"
|
||||
assert_file_contains "docker-compose.yml" "<<<<<<<" "conflict open marker written"
|
||||
assert_file_contains "docker-compose.yml" "=======" "conflict separator written"
|
||||
assert_file_contains "docker-compose.yml" ">>>>>>>" "conflict close marker written"
|
||||
assert_file_contains "docker-compose.yml" "USER-PINNED" "user value present in conflict"
|
||||
assert_file_contains "docker-compose.yml" "supabase/studio:NEW" "upstream value present in conflict"
|
||||
assert_file_contains "docker-compose.yml" "supabase/postgres:17" "non-conflicting line merged (pg17)"
|
||||
assert_path_exists "new-only.txt" "new upstream file added"
|
||||
assert_path_exists "old-only.txt" "removed-upstream file left in place"
|
||||
assert_file_contains "volumes/api/kong.yml" "user added line" "clean merge preserved user line"
|
||||
assert_file_contains ".supabase-version" "ref=self-hosted/v0.9.0" "stamp NOT advanced on conflict"
|
||||
assert_file_contains "$WORK/apply.log" "Files with merge conflicts" "conflicts reported in summary"
|
||||
assert_file_contains "$WORK/apply.log" "[1.0.0]" "manifest entry 1.0.0 surfaced"
|
||||
assert_file_contains "$WORK/apply.log" "[1.1.0] BREAKING" "requires-less last entry surfaced (no set -e abort)"
|
||||
assert_file_missing_pattern "$WORK/apply.log" "[0.9.0]" "base-version entry excluded (window lower bound is half-open)"
|
||||
assert_file_contains "$WORK/apply.log" "Run the demo migration first." "manifest gate step surfaced"
|
||||
assert_file_contains "$WORK/apply.log" "utils/demo-migrate.sh" "manifest gate script surfaced"
|
||||
assert_file_contains "$WORK/apply.log" "example.test/guide" "manifest migration guide surfaced"
|
||||
assert_file_contains "$WORK/apply.log" "gone from the new .env.example" ".env key-removal section shown"
|
||||
assert_file_contains "$WORK/apply.log" "EXTRA_USER_KEY" "removed .env key listed in report"
|
||||
if ls backups/*.tgz >/dev/null 2>&1; then
|
||||
ok "backup archive created"
|
||||
for _bk in backups/*.tgz; do break; done
|
||||
tar tzf "$_bk" > "$WORK/bk.list" 2>/dev/null || true
|
||||
assert_file_contains "$WORK/bk.list" ".env" "backup includes .env"
|
||||
assert_file_missing_pattern "$WORK/bk.list" "volumes/db/data" "backup excludes db data dir"
|
||||
assert_file_missing_pattern "$WORK/bk.list" "volumes/storage" "backup excludes storage dir"
|
||||
assert_file_missing_pattern "$WORK/bk.list" "backups/" "backup excludes backups dir"
|
||||
else
|
||||
bad "no backup archive"
|
||||
fi
|
||||
assert_file_contains "volumes/snippets/user.sql" "USER_SNIPPET" "snippets left untouched"
|
||||
# seed.sql and hello/index.ts are the only files that are BOTH shipped in the
|
||||
# target snapshot AND gitignored, so they are the real is_excluded coverage.
|
||||
# Assert the user's content survives AND no vendor content / conflict markers
|
||||
# leaked in - i.e. the file was skipped, not merged/conflicted.
|
||||
assert_file_contains "volumes/snippets/seed.sql" "USER SEED" "gitignored snippet: user content kept"
|
||||
assert_file_missing_pattern "volumes/snippets/seed.sql" "VENDOR SEED" "gitignored snippet: no vendor content"
|
||||
assert_file_missing_pattern "volumes/snippets/seed.sql" "<<<<<<<" "gitignored snippet: not conflicted (skipped)"
|
||||
assert_file_contains "volumes/functions/my-fn/index.ts" "user fn" "custom edge fn left untouched"
|
||||
assert_file_contains "volumes/functions/main/index.ts" "target main" "vendor main/index.ts updated"
|
||||
assert_file_contains "volumes/functions/hello/index.ts" "user hello" "gitignored fn: user content kept"
|
||||
assert_file_missing_pattern "volumes/functions/hello/index.ts" "target hello" "gitignored fn: no vendor content"
|
||||
assert_file_missing_pattern "volumes/functions/hello/index.ts" "<<<<<<<" "gitignored fn: not conflicted (skipped)"
|
||||
|
||||
echo ""
|
||||
echo "=== update.sh: clean apply (no conflict) advances the stamp and exits 0 ==="
|
||||
|
||||
CLEAN="$WORK/clean"
|
||||
make_deploy "$CLEAN"
|
||||
cd "$CLEAN"
|
||||
rc=0
|
||||
SUPABASE_REPO_URL="$SRC" sh ./update.sh --to self-hosted/v1.1.0 --yes > "$WORK/clean.log" 2>&1 || rc=$?
|
||||
if [ "$rc" = "0" ]; then ok "clean apply exits 0"; else bad "clean apply expected exit 0, got $rc"; fi
|
||||
assert_file_contains "$WORK/clean.log" "Update applied cleanly." "clean apply announced"
|
||||
assert_file_missing_pattern "docker-compose.yml" "<<<<<<<" "clean apply wrote no conflict markers"
|
||||
assert_file_contains ".supabase-version" "ref=self-hosted/v1.1.0" "stamp advanced on clean apply"
|
||||
assert_file_contains ".env" "NEW_KEY=new-default" "new .env key appended (clean)"
|
||||
assert_file_contains "docker-compose.yml" "supabase/studio:NEW" "vendor file updated (clean)"
|
||||
assert_file_contains "volumes/api/kong.yml" "user added line" "clean merge preserved user line (clean)"
|
||||
|
||||
echo ""
|
||||
echo "=== update.sh: default target resolves to latest self-hosted/v* tag ==="
|
||||
|
||||
TAGD="$WORK/tagdefault"
|
||||
make_deploy "$TAGD"
|
||||
cd "$TAGD"
|
||||
SUPABASE_REPO_URL="$SRC" sh ./update.sh --yes > "$WORK/tag.log" 2>&1 || true
|
||||
assert_file_contains "$WORK/tag.log" "Latest release tag: self-hosted/v1.1.0" "resolved latest release tag (no --to)"
|
||||
assert_file_contains ".supabase-version" "ref=self-hosted/v1.1.0" "stamp advanced to the tag"
|
||||
assert_file_contains ".env" "NEW_KEY=new-default" "update applied via default target"
|
||||
|
||||
echo ""
|
||||
echo "=== update.sh: --from supplies the base when the stamp is missing ==="
|
||||
|
||||
FROMD="$WORK/fromd"
|
||||
make_deploy "$FROMD"
|
||||
cd "$FROMD"
|
||||
rm -f .supabase-version
|
||||
rc=0
|
||||
SUPABASE_REPO_URL="$SRC" sh ./update.sh --from self-hosted/v0.9.0 --to self-hosted/v1.1.0 --yes > "$WORK/from.log" 2>&1 || rc=$?
|
||||
assert_file_missing_pattern "$WORK/from.log" "REPORT-ONLY" "--from performs a real update (not report-only)"
|
||||
assert_file_contains ".env" "NEW_KEY=new-default" "--from applied the update"
|
||||
assert_file_contains ".supabase-version" "ref=self-hosted/v1.1.0" "--from advanced the stamp"
|
||||
|
||||
echo ""
|
||||
echo "=== update.sh: --dry-run writes nothing ==="
|
||||
|
||||
DRYD="$WORK/dry"
|
||||
make_deploy "$DRYD" conflict
|
||||
cd "$DRYD"
|
||||
SUPABASE_REPO_URL="$SRC" sh ./update.sh --to self-hosted/v1.1.0 --dry-run > "$WORK/dry.log" 2>&1 || true
|
||||
assert_file_missing_pattern ".env" "NEW_KEY" "dry-run did not append env key"
|
||||
assert_file_missing_pattern "docker-compose.yml" "<<<<<<<" "dry-run did not write conflict"
|
||||
assert_file_contains ".supabase-version" "ref=self-hosted/v0.9.0" "dry-run left stamp unchanged"
|
||||
assert_path_absent "backups" "dry-run took no backup"
|
||||
assert_file_contains "$WORK/dry.log" "DRY RUN" "dry-run labeled output"
|
||||
|
||||
echo ""
|
||||
echo "=== update.sh: missing stamp -> report-only (still surfaces the gate) ==="
|
||||
|
||||
MISS="$WORK/miss"
|
||||
make_deploy "$MISS"
|
||||
cd "$MISS"
|
||||
rm -f .supabase-version
|
||||
rc=0
|
||||
SUPABASE_REPO_URL="$SRC" sh ./update.sh --to self-hosted/v1.1.0 > "$WORK/miss.log" 2>&1 || rc=$?
|
||||
if [ "$rc" = "0" ]; then ok "report-only exits 0"; else bad "report-only expected exit 0, got $rc"; fi
|
||||
assert_file_contains "$WORK/miss.log" "REPORT-ONLY" "report-only mode announced"
|
||||
assert_file_missing_pattern ".env" "NEW_KEY" "report-only wrote nothing to .env"
|
||||
assert_path_absent "backups" "report-only took no backup"
|
||||
assert_file_contains "$WORK/miss.log" "Breaking changes / required manual steps" "report-only surfaces the gate"
|
||||
assert_file_contains "$WORK/miss.log" "[1.0.0]" "report-only lists in-range breaking release"
|
||||
assert_file_contains "$WORK/miss.log" "[0.9.0]" "report-only (open lower bound) includes the base-version entry"
|
||||
|
||||
echo ""
|
||||
echo "=== update.sh: malformed manifest -> refuses (dies), writes nothing ==="
|
||||
|
||||
BADM="$WORK/badmanifest"
|
||||
make_deploy "$BADM"
|
||||
cd "$BADM"
|
||||
rc=0
|
||||
SUPABASE_REPO_URL="$SRC" sh ./update.sh --to malformed-manifest --yes > "$WORK/bad.log" 2>&1 || rc=$?
|
||||
if [ "$rc" != "0" ] && [ "$rc" != "2" ]; then ok "malformed manifest aborts (die, not a normal exit)"; else bad "expected die (non-0, non-2), got $rc"; fi
|
||||
assert_file_contains "$WORK/bad.log" "not a valid JSON object" "malformed-manifest error surfaced"
|
||||
assert_file_missing_pattern ".env" "NEW_KEY" "malformed manifest: .env untouched"
|
||||
assert_path_absent "backups" "malformed manifest: no backup taken"
|
||||
assert_file_contains ".supabase-version" "ref=self-hosted/v0.9.0" "malformed manifest: stamp not advanced"
|
||||
|
||||
echo ""
|
||||
echo "=== update.sh: never overwrites the running script; stages the target's copy as .dist ==="
|
||||
|
||||
# A dedicated upstream whose docker/ ships an update.sh that DIFFERS from the one
|
||||
# we run, so the merge must divert it to update.sh.dist rather than rewrite the
|
||||
# live script mid-run (which would corrupt this process).
|
||||
SELFSRC="$WORK/selfsrc"
|
||||
mkdir -p "$SELFSRC/docker"
|
||||
cd "$SELFSRC"
|
||||
git init -q
|
||||
git config user.email t@t.t
|
||||
git config user.name t
|
||||
printf 'services:\n db:\n image: x\n' > docker/docker-compose.yml
|
||||
printf 'KEEP=1\n' > docker/.env.example
|
||||
cp "$DOCKER_DIR/.gitignore" docker/.gitignore
|
||||
printf '#!/bin/sh\necho THIS-IS-THE-NEW-UPDATE-SH\n' > docker/update.sh
|
||||
git add -A && git commit -qm self && git tag self-hosted/v2.0.0
|
||||
|
||||
SELFDEP="$WORK/selfdep"
|
||||
mkdir -p "$SELFDEP"
|
||||
printf 'services:\n db:\n image: x\n' > "$SELFDEP/docker-compose.yml"
|
||||
printf 'KEEP=1\n' > "$SELFDEP/.env"
|
||||
cp "$UPDATE_SH" "$SELFDEP/update.sh" # the running script = the real update.sh
|
||||
printf 'ref=self-hosted/v2.0.0\n' > "$SELFDEP/.supabase-version"
|
||||
cd "$SELFDEP"
|
||||
rc=0
|
||||
SUPABASE_REPO_URL="$SELFSRC" sh ./update.sh --to self-hosted/v2.0.0 --yes > "$WORK/self.log" 2>&1 || rc=$?
|
||||
if [ "$rc" = "0" ]; then ok "self-update run exits 0"; else bad "expected exit 0, got $rc"; fi
|
||||
if cmp -s ./update.sh "$UPDATE_SH"; then ok "running update.sh left byte-identical"; else bad "running update.sh was modified in place"; fi
|
||||
assert_no_line "./update.sh" '^(<<<<<<<|=======|>>>>>>>)' "no conflict markers written into the running update.sh"
|
||||
assert_path_exists "$SELFDEP/update.sh.dist" "target's update.sh staged as update.sh.dist"
|
||||
assert_file_contains "$SELFDEP/update.sh.dist" "THIS-IS-THE-NEW-UPDATE-SH" "update.sh.dist holds the target's version"
|
||||
assert_file_contains "$WORK/self.log" "update.sh.dist" "summary points the user at update.sh.dist"
|
||||
|
||||
# --- summary -----------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=== Result: $PASS passed, $FAIL failed ==="
|
||||
[ "$FAIL" = "0" ] || exit 1
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Validate the upgrade manifest (upgrades.json), which update.sh reads with jq.
|
||||
#
|
||||
# - json: upgrades.json is valid JSON
|
||||
# - keys: top-level keys are bare-semver versions (e.g. "0.7.0"), plus the
|
||||
# optional "_schema" documentation block
|
||||
# - schema: each version-keyed entry has only known fields, with valid types
|
||||
# (an unknown/misspelled key like "breakng" would silently disarm
|
||||
# its gate, so it is rejected here)
|
||||
# - gate: any non-null "gate" points at a script that exists in the repo
|
||||
#
|
||||
# The manifest is the source of truth for gating; the CHANGELOG is display only,
|
||||
# so this test deliberately does NOT cross-check the two. Requires jq (already a
|
||||
# runtime dependency of update.sh); no yq, no generation step.
|
||||
#
|
||||
# Usage:
|
||||
# sh tests/test-upgrades-manifest.sh # run from the docker/ directory
|
||||
#
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
DOCKER_DIR=$(dirname "$SCRIPT_DIR")
|
||||
cd "$DOCKER_DIR"
|
||||
|
||||
JSON=upgrades.json
|
||||
|
||||
command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is required"; exit 1; }
|
||||
[ -f "$JSON" ] || { echo "ERROR: $JSON missing"; exit 1; }
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT INT TERM
|
||||
|
||||
PASS=0; FAIL=0
|
||||
ok() { PASS=$((PASS+1)); printf " ok - %s\n" "$1"; }
|
||||
bad() { FAIL=$((FAIL+1)); printf " FAIL - %s\n" "$1"; }
|
||||
|
||||
echo ""
|
||||
echo "=== upgrades.json is valid JSON ==="
|
||||
if jq -e . "$JSON" >/dev/null 2>"$TMP/err"; then
|
||||
ok "upgrades.json parses"
|
||||
else
|
||||
bad "upgrades.json is not valid JSON: $(cat "$TMP/err" 2>/dev/null)"
|
||||
echo "=== Result: $PASS passed, $FAIL failed ==="; exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== top-level keys are versions (or _schema) ==="
|
||||
for k in $(jq -r 'keys[]' "$JSON"); do
|
||||
case "$k" in
|
||||
_schema) ok "doc block '_schema' present" ;;
|
||||
[0-9]*.[0-9]*) ok "version key $k" ;;
|
||||
*) bad "unexpected top-level key '$k' (want bare semver like 0.7.0)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== version-keyed entries have only known fields, with valid types ==="
|
||||
for k in $(jq -r 'keys[]' "$JSON"); do
|
||||
case "$k" in [0-9]*.[0-9]*) ;; *) continue ;; esac
|
||||
errs=$(jq -r --arg k "$k" '.[$k] as $e
|
||||
| (($e | keys) - ["breaking", "gate", "migration_guide_url", "requires"]) as $unknown
|
||||
| [ (if ($e.breaking != null) and (($e.breaking|type) != "boolean") then "breaking must be bool" else empty end),
|
||||
(if ($e.gate != null) and (($e.gate|type) != "string") then "gate must be string|null" else empty end),
|
||||
(if ($e.migration_guide_url != null) and (($e.migration_guide_url|type) != "string") then "migration_guide_url must be string|null" else empty end),
|
||||
(if ($e.requires != null) and (($e.requires|type) != "array") then "requires must be array" else empty end),
|
||||
(if ($unknown | length) > 0 then "unknown field(s) (typo?): " + ($unknown | join(", ")) else empty end)
|
||||
] | join("; ")' "$JSON")
|
||||
if [ -z "$errs" ]; then ok "entry $k valid"; else bad "entry $k: $errs"; fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== gate scripts referenced by entries exist ==="
|
||||
checked=0
|
||||
for k in $(jq -r 'keys[]' "$JSON"); do
|
||||
case "$k" in [0-9]*.[0-9]*) ;; *) continue ;; esac
|
||||
gate=$(jq -r --arg k "$k" '.[$k].gate // empty' "$JSON")
|
||||
[ -n "$gate" ] || continue
|
||||
checked=$((checked+1))
|
||||
if [ -f "$gate" ]; then
|
||||
ok "gate for $k exists: $gate"
|
||||
else
|
||||
bad "gate for $k missing: $gate"
|
||||
fi
|
||||
done
|
||||
[ "$checked" = 0 ] && echo " (no entries reference a gate script)"
|
||||
|
||||
echo ""
|
||||
echo "=== Result: $PASS passed, $FAIL failed ==="
|
||||
[ "$FAIL" = 0 ] || exit 1
|
||||
Executable
+692
@@ -0,0 +1,692 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Update an existing self-hosted Supabase deployment in place.
|
||||
#
|
||||
# The deployment directory mixes vendor-owned files (docker-compose.yml, the
|
||||
# override files, volumes/*, scripts, .env.example) with user-owned state
|
||||
# (.env, docker-compose.override.yml, volumes/db/data, volumes/storage, etc.).
|
||||
# This script pulls a newer version of the Supabase files on top of yours
|
||||
# using a 3-way merge against the version you started from, so local edits
|
||||
# survive and genuine conflicts are surfaced rather than silently overwritten.
|
||||
#
|
||||
# The version you started from is recorded in .supabase-version (written by
|
||||
# setup.sh). If it is missing, pass --from <ref> or follow the printed guidance.
|
||||
#
|
||||
# What it never touches: .env values you set, docker-compose.override.yml, and
|
||||
# the data directories (volumes/db/data, volumes/storage, etc.). New keys from
|
||||
# .env.example are appended to your .env; existing values are kept as-is.
|
||||
#
|
||||
# By default it updates to the latest self-hosted/v* release tag (or 'master'
|
||||
# until the first tag exists). Pass --to to pin a specific tag/branch.
|
||||
#
|
||||
# Usage:
|
||||
# sh update.sh # update to the latest release tag
|
||||
# sh update.sh --dry-run # show what would change, write nothing
|
||||
# sh update.sh --to <tag> # update to a specific tag/branch
|
||||
# sh update.sh --from <ref> # base to merge from (if no .supabase-version)
|
||||
# sh update.sh --yes # don't prompt, even on breaking changes
|
||||
#
|
||||
# Env:
|
||||
# SUPABASE_REPO_URL Override the upstream repo (default: github supabase/supabase)
|
||||
#
|
||||
# Documentation: https://supabase.com/docs/guides/self-hosting/updating
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Pipeline (see main at the bottom):
|
||||
# resolve refs -> fetch base+target snapshots -> [report-only exit]
|
||||
# -> build manifest gate -> confirm_gate (before any writes)
|
||||
# -> backup → merge vendor files + .env keys -> summary → stamp
|
||||
#
|
||||
# Three trees for every vendor file path:
|
||||
# - base - upstream at BASE_REF (.supabase-version ref=, or --from)
|
||||
# - target - upstream at TARGET_REF (--to, or latest self-hosted/v* tag)
|
||||
# - user's - the deployment directory (cwd); _not_ a git checkout
|
||||
#
|
||||
# Git is only used to fetch snapshots (fetch_snapshot) and to run git merge-file.
|
||||
#
|
||||
# Breaking-change gate (upgrades.json on the target snapshot):
|
||||
# - Keyed by version (e.g. "0.7.0"); window = entries in (BASE_VER, TARGET_VER],
|
||||
# where the bounds are the base/target refs reduced to bare semver
|
||||
# (self-hosted/vX.Y.Z -> X.Y.Z) and compared with sort -V.
|
||||
# - If a bound is not a release tag (a commit SHA, or "master"), that side of
|
||||
# the window is left open and all applicable entries are shown with a warning.
|
||||
# - Prompt when an entry has breaking:true or a gate script; runs before
|
||||
# backup/merge so abort leaves the deployment untouched.
|
||||
# - CHANGELOG.md is never parsed (update.sh only points users at it); routine
|
||||
# "requires compose update" items are applied by the merge, not listed here.
|
||||
#
|
||||
# User-owned paths skipped during merge are defined in .gitignore (loaded from the
|
||||
# target snapshot). git check-ignore --no-index applies negation rules (e.g.
|
||||
# volumes/functions/** ignored except volumes/functions/main/index.ts).
|
||||
# .git/ is excluded here only - never listed in .gitignore.
|
||||
#
|
||||
# .env is never 3-way merged: append missing keys from .env.example only.
|
||||
|
||||
# --- globals (set during main) -----------------------------------------------
|
||||
|
||||
REPO_URL="${SUPABASE_REPO_URL:-https://github.com/supabase/supabase}"
|
||||
STAMP_FILE=".supabase-version"
|
||||
SELF_NAME=$(basename "$0")
|
||||
DRY_RUN=0
|
||||
ASSUME_YES=0
|
||||
TO_REF=""
|
||||
FROM_REF=""
|
||||
|
||||
TARGET_REF=""
|
||||
BASE_REF=""
|
||||
BASE_VER=""
|
||||
TARGET_VER=""
|
||||
REPORT_ONLY=0
|
||||
|
||||
TMP_ROOT=""
|
||||
TARGET_DIR=""
|
||||
BASE_DIR=""
|
||||
REPORT=""
|
||||
ENV_ADDED=""
|
||||
ENV_REMOVED=""
|
||||
GATE_REPORT=""
|
||||
GATE_REQUIRED=0
|
||||
IGNORE_FILE=""
|
||||
IGNORE_GIT_DIR=""
|
||||
|
||||
# --- logging -----------------------------------------------------------------
|
||||
|
||||
log() { printf "===> %s\n" "$*"; }
|
||||
warn() { printf "WARNING: %s\n" "$*" >&2; }
|
||||
die() { printf "ERROR: %s\n" "$*" >&2; exit 1; }
|
||||
|
||||
print_help() {
|
||||
awk 'NR==1 {next} /^#/ {sub(/^# ?/,""); print; next} {exit}' "$0"
|
||||
}
|
||||
|
||||
# --- small helpers -----------------------------------------------------------
|
||||
|
||||
# load_ignore_file - vendor/user split from the target snapshot's .gitignore.
|
||||
# Uses a throwaway git dir so check-ignore works on deployment trees that are
|
||||
# not git repos (and on Apple Git, which rejects --git-dir=/dev/null).
|
||||
load_ignore_file() {
|
||||
if [ -f "$TARGET_DIR/.gitignore" ]; then
|
||||
IGNORE_FILE="$TARGET_DIR/.gitignore"
|
||||
elif [ -f .gitignore ]; then
|
||||
IGNORE_FILE="$(pwd)/.gitignore"
|
||||
else
|
||||
IGNORE_FILE=""
|
||||
warn "No .gitignore found; only .git paths are excluded from the merge."
|
||||
return 0
|
||||
fi
|
||||
case "$IGNORE_FILE" in
|
||||
/*) ;;
|
||||
*) IGNORE_FILE="$(cd "$(dirname "$IGNORE_FILE")" && pwd)/$(basename "$IGNORE_FILE")" ;;
|
||||
esac
|
||||
IGNORE_GIT_DIR="$TMP_ROOT/ignore-git"
|
||||
git init -q "$IGNORE_GIT_DIR"
|
||||
}
|
||||
|
||||
# is_excluded <relpath> - true when the path is user-owned and must not merge.
|
||||
is_excluded() {
|
||||
case "$1" in
|
||||
.git|.git/*) return 0 ;;
|
||||
esac
|
||||
[ -n "$IGNORE_FILE" ] || return 1
|
||||
git -C "$IGNORE_GIT_DIR" -c "core.excludesfile=$IGNORE_FILE" \
|
||||
check-ignore -q --no-index "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
read_stamp_ref() {
|
||||
[ -f "$STAMP_FILE" ] || return 1
|
||||
val=$(grep -E '^ref=' "$STAMP_FILE" 2>/dev/null | head -n1 | cut -d= -f2- | tr -d "\r\"' ")
|
||||
if [ -z "$val" ]; then
|
||||
val=$(grep -vE '^[[:space:]]*#' "$STAMP_FILE" 2>/dev/null \
|
||||
| grep -vE '^[[:space:]]*$' | head -n1 | tr -d "\r\"' ")
|
||||
fi
|
||||
[ -n "$val" ] && printf '%s' "$val"
|
||||
}
|
||||
|
||||
# env_has_key <key> <file> - true if the key appears as KEY=, even commented
|
||||
# (e.g. "#GOOGLE_ENABLED="), so we never re-add a key the user disabled on purpose.
|
||||
env_has_key() {
|
||||
grep -qE "^[[:space:]]*#?[[:space:]]*$1=" "$2" 2>/dev/null
|
||||
}
|
||||
|
||||
record() { printf '%s:%s\n' "$1" "$2" >> "$REPORT"; }
|
||||
|
||||
count_status() { grep -cE "^$1:" "$REPORT" 2>/dev/null || true; }
|
||||
|
||||
list_status() {
|
||||
grep -E "^$1:" "$REPORT" 2>/dev/null | cut -d: -f2- | sed 's/^/ /'
|
||||
}
|
||||
|
||||
# --- upstream snapshots ------------------------------------------------------
|
||||
|
||||
_sparse_init() {
|
||||
git -C "$1" init -q
|
||||
git -C "$1" remote add origin "$REPO_URL"
|
||||
git -C "$1" config core.sparseCheckout true
|
||||
git -C "$1" sparse-checkout init --cone >/dev/null 2>&1
|
||||
git -C "$1" sparse-checkout set docker >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# fetch_snapshot <ref> <dest_dir>
|
||||
# Materializes ./docker at <ref> into <dest_dir> via a shallow fetch. Also the
|
||||
# seam an artifact source (tarball + sha256) would slot into later.
|
||||
fetch_snapshot() {
|
||||
_ref="$1"
|
||||
_dest="$2"
|
||||
_work=$(mktemp -d "$TMP_ROOT/fetch.XXXXXX")
|
||||
if _sparse_init "$_work" \
|
||||
&& git -C "$_work" fetch --depth=1 --filter=blob:none -q origin "$_ref" 2>/dev/null \
|
||||
&& git -C "$_work" checkout -q FETCH_HEAD 2>/dev/null \
|
||||
&& [ -d "$_work/docker" ]; then
|
||||
mkdir -p "$_dest"
|
||||
cp -rf "$_work/docker/." "$_dest/"
|
||||
rm -rf "$_work"
|
||||
else
|
||||
rm -rf "$_work"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
latest_release_tag() {
|
||||
git ls-remote --tags --refs "$REPO_URL" 2>/dev/null \
|
||||
| sed 's#^.*refs/tags/##' \
|
||||
| grep -E '^self-hosted/v[0-9]' \
|
||||
| sort -V | tail -n1
|
||||
}
|
||||
|
||||
list_files() {
|
||||
( cd "$1" && find . -type f | sed 's|^\./||' | grep -vE '^\.git(/|$)' | sort )
|
||||
}
|
||||
|
||||
# normalize_version <ref> - reduce a ref to bare semver (0.7.0) for comparison,
|
||||
# or empty when it is not a self-hosted release tag (commit SHA, "master", …).
|
||||
normalize_version() {
|
||||
_v="${1#refs/tags/}"
|
||||
_v="${_v#self-hosted/}"
|
||||
_v="${_v#v}"
|
||||
case "$_v" in
|
||||
[0-9]*.[0-9]*) printf '%s' "$_v" ;;
|
||||
*) ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ver_gt A B - true when version A is strictly greater than B (sort -V order).
|
||||
ver_gt() {
|
||||
if [ "$1" = "$2" ]; then return 1; fi
|
||||
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ]
|
||||
}
|
||||
|
||||
# ver_in_window VER - true when BASE_VER < VER <= TARGET_VER. An empty bound
|
||||
# leaves that side open (over-reports a gate rather than hiding one).
|
||||
ver_in_window() {
|
||||
if [ -n "$TARGET_VER" ] && ver_gt "$1" "$TARGET_VER"; then return 1; fi
|
||||
if [ -n "$BASE_VER" ] && ! ver_gt "$1" "$BASE_VER"; then return 1; fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- ref resolution ----------------------------------------------------------
|
||||
|
||||
resolve_target_ref() {
|
||||
if [ -n "$TO_REF" ]; then
|
||||
TARGET_REF="$TO_REF"
|
||||
return
|
||||
fi
|
||||
TARGET_REF=$(latest_release_tag)
|
||||
if [ -n "$TARGET_REF" ]; then
|
||||
log "Latest release tag: $TARGET_REF"
|
||||
else
|
||||
TARGET_REF="master"
|
||||
warn "No self-hosted/v* release tags found; targeting 'master'. Pin a version with --to."
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_base_ref() {
|
||||
REPORT_ONLY=0
|
||||
if [ -n "$FROM_REF" ]; then
|
||||
BASE_REF="$FROM_REF"
|
||||
elif BASE_REF=$(read_stamp_ref) && [ -n "$BASE_REF" ]; then
|
||||
:
|
||||
else
|
||||
REPORT_ONLY=1
|
||||
BASE_REF=""
|
||||
fi
|
||||
}
|
||||
|
||||
print_report_only_guidance() {
|
||||
warn "No $STAMP_FILE found and no --from <ref> given; cannot determine the version you started from."
|
||||
cat >&2 <<EOF
|
||||
|
||||
Without a base version a safe 3-way merge is not possible. To fix this, find the
|
||||
version your deployment was based on and record it, then re-run:
|
||||
|
||||
1. Identify your base - the self-hosted/vX.Y.Z release (or commit) your files
|
||||
came from. Cross-reference the image tags in docker-compose.yml / .env
|
||||
against versions.md, or the newest CHANGELOG.md section you remember pulling.
|
||||
2. Write it to $STAMP_FILE, e.g.:
|
||||
printf 'ref=self-hosted/v0.7.0\n' > $STAMP_FILE
|
||||
3. Re-run: sh update.sh
|
||||
|
||||
Or supply it inline for this run: sh update.sh --from <commit-sha-or-tag>
|
||||
|
||||
Continuing in REPORT-ONLY mode. NOTE: this is NOT the full set of changes -
|
||||
without a base version it can only list files and .env keys that are entirely
|
||||
NEW to you; it CANNOT show which existing files would change or conflict. Record
|
||||
a base and re-run for the real preview. Nothing will be written.
|
||||
EOF
|
||||
}
|
||||
|
||||
fetch_snapshots() {
|
||||
TARGET_DIR="$TMP_ROOT/target"
|
||||
log "Fetching target snapshot ($TARGET_REF)"
|
||||
fetch_snapshot "$TARGET_REF" "$TARGET_DIR" \
|
||||
|| die "Could not fetch target snapshot '$TARGET_REF' from $REPO_URL"
|
||||
|
||||
if [ "$REPORT_ONLY" = "1" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
BASE_DIR="$TMP_ROOT/base"
|
||||
log "Fetching base snapshot ($BASE_REF)"
|
||||
fetch_snapshot "$BASE_REF" "$BASE_DIR" \
|
||||
|| die "Could not fetch base snapshot '$BASE_REF' from $REPO_URL"
|
||||
}
|
||||
|
||||
run_report_only() {
|
||||
log "Files in '$TARGET_REF' you do NOT have yet (brand-new only; existing files that changed are NOT shown here):"
|
||||
while IFS= read -r f; do
|
||||
is_excluded "$f" && continue
|
||||
[ -f "$f" ] || echo " + $f"
|
||||
done <<EOF
|
||||
$(list_files "$TARGET_DIR")
|
||||
EOF
|
||||
echo ""
|
||||
log ".env keys in the new .env.example that your .env is missing (add these):"
|
||||
if [ -f "$TARGET_DIR/.env.example" ]; then
|
||||
while IFS= read -r k; do
|
||||
env_has_key "$k" .env || echo " + $k"
|
||||
done <<EOF
|
||||
$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$TARGET_DIR/.env.example" | cut -d= -f1)
|
||||
EOF
|
||||
fi
|
||||
echo ""
|
||||
warn "This is NOT the full update. To see what would actually change, record a base version."
|
||||
warn "See the guidance above and re-run the script. Nothing was written."
|
||||
}
|
||||
|
||||
# --- breaking-change gate (manifest-driven, before any writes) ---------------
|
||||
# Reads docker/upgrades.json from the target snapshot; gating keys off manifest
|
||||
# entries in (BASE_VER, TARGET_VER].
|
||||
|
||||
build_gate_report() {
|
||||
_manifest="$TARGET_DIR/upgrades.json"
|
||||
GATE_REQUIRED=0
|
||||
: > "$GATE_REPORT"
|
||||
|
||||
if [ ! -f "$_manifest" ]; then
|
||||
return 0
|
||||
fi
|
||||
if ! jq -e 'type == "object"' "$_manifest" >/dev/null 2>&1; then
|
||||
die "$_manifest is present but is not a valid JSON object; refusing to update without a working breaking-change gate. Please report this to the maintainers."
|
||||
fi
|
||||
|
||||
if [ -z "$BASE_VER" ] || [ -z "$TARGET_VER" ]; then
|
||||
warn "Base or target is not a self-hosted/vX.Y.Z tag; cannot compute an exact update window."
|
||||
warn "Showing all applicable manual-action releases."
|
||||
warn "Review which ones apply to your deployment."
|
||||
fi
|
||||
|
||||
for k in $(jq -r 'keys[]' "$_manifest" 2>/dev/null); do
|
||||
case "$k" in [0-9]*.[0-9]*) ;; *) continue ;; esac
|
||||
ver_in_window "$k" || continue
|
||||
|
||||
breaking=$(jq -r --arg k "$k" '.[$k].breaking // false' "$_manifest")
|
||||
gate=$(jq -r --arg k "$k" '.[$k].gate // empty' "$_manifest")
|
||||
url=$(jq -r --arg k "$k" '.[$k].migration_guide_url // empty' "$_manifest")
|
||||
reqs=$(jq -r --arg k "$k" '.[$k].requires[]? // empty' "$_manifest")
|
||||
|
||||
if [ "$breaking" = "true" ] || [ -n "$gate" ]; then
|
||||
GATE_REQUIRED=1
|
||||
fi
|
||||
|
||||
{
|
||||
[ "$breaking" = "true" ] && echo "[$k] BREAKING" || echo "[$k]"
|
||||
[ -n "$gate" ] && echo " gate: '$gate' must be run first (see the steps below for the exact command)"
|
||||
[ -n "$url" ] && echo " guide: $url"
|
||||
[ -n "$reqs" ] && printf '%s\n' "$reqs" | sed 's/^/ - /'
|
||||
} >> "$GATE_REPORT"
|
||||
done
|
||||
# Explicit success: the loop's last iteration can end on a false test (e.g.
|
||||
# an entry with no 'requires'), which would otherwise make this function
|
||||
# return non-zero and abort the whole script under 'set -e'.
|
||||
return 0
|
||||
}
|
||||
|
||||
confirm_gate() {
|
||||
[ "$GATE_REQUIRED" = "1" ] || return 0
|
||||
[ "$DRY_RUN" != "1" ] || return 0
|
||||
[ "$ASSUME_YES" != "1" ] || return 0
|
||||
|
||||
echo "" >&2
|
||||
warn "This update requires manual action - review before continuing:"
|
||||
sed 's/^/ /' "$GATE_REPORT" >&2
|
||||
echo "" >&2
|
||||
|
||||
if { : > /dev/tty; } 2>/dev/null; then
|
||||
printf "Have you completed the required steps and want to continue? [y/N]: " > /dev/tty
|
||||
read -r reply < /dev/tty
|
||||
case "$reply" in
|
||||
y|Y|yes|YES) return 0 ;;
|
||||
*) die "Aborted by user. Nothing was modified." ;;
|
||||
esac
|
||||
fi
|
||||
die "Breaking/gated changes present and no controlling terminal to confirm. Re-run with --yes to proceed."
|
||||
}
|
||||
|
||||
# --- backup ------------------------------------------------------------------
|
||||
|
||||
take_backup() {
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
log "Dry run: no backup taken, nothing will be written."
|
||||
return 0
|
||||
fi
|
||||
mkdir -p backups
|
||||
_backup="backups/pre-update-$(date +%Y%m%d-%H%M%S).tgz"
|
||||
log "Backing up current configuration to $_backup (excluding data directories)"
|
||||
tar czf "$_backup" \
|
||||
--exclude='./backups' \
|
||||
--exclude='./volumes/db/data' \
|
||||
--exclude='./volumes/storage' \
|
||||
. 2>/dev/null || warn "Backup archive reported errors; review $_backup before relying on it."
|
||||
warn "This does NOT back up your database. Back it up separately before updating."
|
||||
}
|
||||
|
||||
# --- vendor file merge -------------------------------------------------------
|
||||
|
||||
apply_file() {
|
||||
[ "$DRY_RUN" = "1" ] && return 0
|
||||
_dir=$(dirname "$1")
|
||||
[ "$_dir" = "." ] || mkdir -p "$_dir"
|
||||
cp -f "$2" "$1"
|
||||
}
|
||||
|
||||
# Per-file 3-way merge (u=yours, b=base snapshot, t=target snapshot):
|
||||
# no t → keep u; report removed-upstream
|
||||
# no u → copy t; report new
|
||||
# u == t → report unchanged
|
||||
# u == b → copy t; report updated (user never edited)
|
||||
# else → git merge-file u b t; report merged-clean or CONFLICT
|
||||
# If b had no file, an empty file stands in for b.
|
||||
merge_one_file() {
|
||||
f="$1"
|
||||
empty="$2"
|
||||
merged="$TMP_ROOT/merged.out"
|
||||
|
||||
b="$BASE_DIR/$f"
|
||||
t="$TARGET_DIR/$f"
|
||||
u="$f"
|
||||
|
||||
if [ ! -f "$t" ]; then
|
||||
[ -f "$u" ] && record "removed-upstream" "$f"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$u" ]; then
|
||||
apply_file "$f" "$t"
|
||||
record "new" "$f"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if cmp -s "$u" "$t"; then
|
||||
record "unchanged" "$f"
|
||||
return 0
|
||||
fi
|
||||
|
||||
base_for_merge="$b"
|
||||
[ -f "$base_for_merge" ] || base_for_merge="$empty"
|
||||
|
||||
if cmp -s "$u" "$base_for_merge"; then
|
||||
apply_file "$f" "$t"
|
||||
record "updated" "$f"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if git merge-file -p -q \
|
||||
-L "yours ($f)" -L "base" -L "new ($TARGET_REF)" \
|
||||
"$u" "$base_for_merge" "$t" > "$merged" 2>/dev/null; then
|
||||
[ "$DRY_RUN" != "1" ] && cp -f "$merged" "$u"
|
||||
record "merged-clean" "$f"
|
||||
elif [ -s "$merged" ]; then
|
||||
# Non-zero exit with output = a normal conflict (markers written).
|
||||
[ "$DRY_RUN" != "1" ] && cp -f "$merged" "$u"
|
||||
record "CONFLICT" "$f"
|
||||
else
|
||||
# git merge-file errored and produced no output; keep the user's file
|
||||
# intact rather than truncating it to empty.
|
||||
record "merge-failed" "$f"
|
||||
fi
|
||||
}
|
||||
|
||||
# The running script is a vendor file too, but overwriting it in place would
|
||||
# corrupt this process (the shell reads $0 as it runs). Never write it directly:
|
||||
# if the target ships a different version, stage it as <name>.dist to review.
|
||||
stage_self_update() {
|
||||
_t="$TARGET_DIR/$SELF_NAME"
|
||||
if [ ! -f "$_t" ] || cmp -s "$SELF_NAME" "$_t"; then
|
||||
record "unchanged" "$SELF_NAME"
|
||||
return 0
|
||||
fi
|
||||
[ "$DRY_RUN" = "1" ] || cp -f "$_t" "$SELF_NAME.dist"
|
||||
record "self-staged" "$SELF_NAME"
|
||||
}
|
||||
|
||||
merge_vendor_files() {
|
||||
_empty="$TMP_ROOT/empty"
|
||||
: > "$_empty"
|
||||
: > "$REPORT"
|
||||
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
is_excluded "$f" && continue
|
||||
if [ "$f" = "$SELF_NAME" ]; then
|
||||
stage_self_update
|
||||
continue
|
||||
fi
|
||||
merge_one_file "$f" "$_empty"
|
||||
done <<EOF
|
||||
$( { list_files "$BASE_DIR"; list_files "$TARGET_DIR"; } | sort -u )
|
||||
EOF
|
||||
}
|
||||
|
||||
# --- .env key-union merge ----------------------------------------------------
|
||||
|
||||
merge_env_file() {
|
||||
_example="$TARGET_DIR/.env.example"
|
||||
: > "$ENV_ADDED"
|
||||
: > "$ENV_REMOVED"
|
||||
[ -f "$_example" ] || return 0
|
||||
|
||||
while IFS= read -r k; do
|
||||
env_has_key "$k" .env || echo "$k" >> "$ENV_ADDED"
|
||||
done <<EOF
|
||||
$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' "$_example" | cut -d= -f1)
|
||||
EOF
|
||||
|
||||
while IFS= read -r k; do
|
||||
env_has_key "$k" "$_example" || echo "$k" >> "$ENV_REMOVED"
|
||||
done <<EOF
|
||||
$(grep -E '^[A-Za-z_][A-Za-z0-9_]*=' .env | cut -d= -f1)
|
||||
EOF
|
||||
|
||||
if [ ! -s "$ENV_ADDED" ] || [ "$DRY_RUN" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "############"
|
||||
echo "# Added by update.sh on $(date +%Y-%m-%d) from .env.example ($TARGET_REF)."
|
||||
echo "# Review and set values as needed."
|
||||
echo "############"
|
||||
while IFS= read -r k; do
|
||||
grep -E "^${k}=" "$_example" | head -n1
|
||||
done < "$ENV_ADDED"
|
||||
} >> .env
|
||||
}
|
||||
|
||||
# --- summary and finish ------------------------------------------------------
|
||||
|
||||
print_summary() {
|
||||
echo ""
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
log "DRY RUN - the following WOULD change (nothing was written):"
|
||||
else
|
||||
log "Update applied. Summary:"
|
||||
fi
|
||||
printf " updated: %s\n" "$(count_status updated)"
|
||||
printf " new: %s\n" "$(count_status new)"
|
||||
printf " merged (clean): %s\n" "$(count_status merged-clean)"
|
||||
printf " CONFLICTS: %s\n" "$(count_status CONFLICT)"
|
||||
printf " merge failures: %s\n" "$(count_status merge-failed)"
|
||||
printf " removed upstream: %s\n" "$(count_status removed-upstream)"
|
||||
printf " env keys added: %s\n" "$( [ -s "$ENV_ADDED" ] && wc -l < "$ENV_ADDED" | tr -d ' ' || echo 0 )"
|
||||
|
||||
if [ "$(count_status CONFLICT)" != "0" ]; then
|
||||
echo ""
|
||||
warn "Files with merge conflicts (edit these and remove the <<<<<<< ======= >>>>>>> markers):"
|
||||
list_status CONFLICT
|
||||
fi
|
||||
if [ "$(count_status merge-failed)" != "0" ]; then
|
||||
echo ""
|
||||
warn "Files git could not merge (left unchanged - update these manually):"
|
||||
list_status merge-failed
|
||||
fi
|
||||
if [ "$(count_status merged-clean)" != "0" ]; then
|
||||
echo ""
|
||||
log "Files merged cleanly (review recommended):"
|
||||
list_status merged-clean
|
||||
fi
|
||||
if [ "$(count_status removed-upstream)" != "0" ]; then
|
||||
echo ""
|
||||
log "Removed upstream but kept in place (you may no longer need these):"
|
||||
list_status removed-upstream
|
||||
fi
|
||||
if [ "$(count_status self-staged)" != "0" ]; then
|
||||
echo ""
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
log "$SELF_NAME differs from the version in '$TARGET_REF'; a real run would stage that version as $SELF_NAME.dist (the running script is never overwritten in place)."
|
||||
else
|
||||
log "$SELF_NAME differs from the version in '$TARGET_REF', staged as $SELF_NAME.dist (the running script was not modified)."
|
||||
log "Review it, then swap it in if needed: mv $SELF_NAME.dist $SELF_NAME"
|
||||
fi
|
||||
fi
|
||||
if [ -s "$ENV_ADDED" ]; then
|
||||
echo ""
|
||||
log ".env keys added (review values):"
|
||||
sed 's/^/ + /' "$ENV_ADDED"
|
||||
fi
|
||||
if [ -s "$ENV_REMOVED" ]; then
|
||||
echo ""
|
||||
log ".env keys you have that are gone from the new .env.example (review/remove manually):"
|
||||
sed 's/^/ - /' "$ENV_REMOVED"
|
||||
fi
|
||||
if [ -s "$GATE_REPORT" ]; then
|
||||
echo ""
|
||||
log "Required manual steps for this update (from upgrades.json):"
|
||||
sed 's/^/ /' "$GATE_REPORT"
|
||||
fi
|
||||
echo ""
|
||||
log "For what changed in this update, see CHANGELOG.md (from $BASE_REF to $TARGET_REF)."
|
||||
}
|
||||
|
||||
write_stamp() {
|
||||
{
|
||||
echo "# Supabase self-hosted version stamp. Managed by setup.sh / update.sh."
|
||||
echo "# Do not commit or edit by hand. Records the ref this deployment was based on."
|
||||
echo "ref=$TARGET_REF"
|
||||
} > "$STAMP_FILE"
|
||||
}
|
||||
|
||||
print_next_steps() {
|
||||
echo ""
|
||||
log "Next steps:"
|
||||
echo " 1. Review the changes above (compare against the latest backup in backups/ if needed)."
|
||||
echo " 2. sh run.sh pull"
|
||||
echo " 3. sh run.sh recreate"
|
||||
}
|
||||
|
||||
# --- main --------------------------------------------------------------------
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--yes|-y) ASSUME_YES=1; shift ;;
|
||||
--to) [ $# -ge 2 ] || die "--to requires a <tag> argument."; TO_REF="$2"; shift 2 ;;
|
||||
--from) [ $# -ge 2 ] || die "--from requires a <ref> argument."; FROM_REF="$2"; shift 2 ;;
|
||||
-h|--help) print_help; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; print_help; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -f docker-compose.yml ] || die "docker-compose.yml not found in $(pwd). Run this from your deployment directory."
|
||||
[ -f .env ] || die ".env not found in $(pwd). This does not look like a configured deployment."
|
||||
command -v git >/dev/null 2>&1 || die "git is required but was not found on PATH."
|
||||
command -v jq >/dev/null 2>&1 || die "jq is required but was not found on PATH."
|
||||
|
||||
resolve_target_ref
|
||||
resolve_base_ref
|
||||
TARGET_VER=$(normalize_version "$TARGET_REF")
|
||||
BASE_VER=$(normalize_version "$BASE_REF")
|
||||
[ "$REPORT_ONLY" = "1" ] && print_report_only_guidance
|
||||
|
||||
TMP_ROOT=$(mktemp -d)
|
||||
REPORT="$TMP_ROOT/report"
|
||||
ENV_ADDED="$TMP_ROOT/env_added"
|
||||
ENV_REMOVED="$TMP_ROOT/env_removed"
|
||||
GATE_REPORT="$TMP_ROOT/gate_report"
|
||||
trap 'rm -rf "$TMP_ROOT"' EXIT INT TERM
|
||||
|
||||
fetch_snapshots
|
||||
load_ignore_file
|
||||
|
||||
if [ "$REPORT_ONLY" = "1" ]; then
|
||||
run_report_only
|
||||
build_gate_report
|
||||
if [ -s "$GATE_REPORT" ]; then
|
||||
echo ""
|
||||
log "Breaking changes / required manual steps in this range (from upgrades.json):"
|
||||
sed 's/^/ /' "$GATE_REPORT"
|
||||
warn "Record a base version (see above) and re-run to apply with the gate enforced."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
build_gate_report
|
||||
confirm_gate
|
||||
|
||||
take_backup
|
||||
merge_vendor_files
|
||||
merge_env_file
|
||||
print_summary
|
||||
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
echo ""
|
||||
log "Dry run complete. Re-run without --dry-run to apply."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(count_status CONFLICT)" != "0" ] || [ "$(count_status merge-failed)" != "0" ]; then
|
||||
echo ""
|
||||
warn "Update applied WITH CONFLICTS. Resolve the files listed above (remove the"
|
||||
warn "<<<<<<< ======= >>>>>>> markers, or fix the files git could not merge)"
|
||||
warn "before starting the stack."
|
||||
warn "The version stamp was NOT advanced; it will update on your next clean run."
|
||||
warn "Exiting with status 2."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
write_stamp
|
||||
print_next_steps
|
||||
log "Update applied cleanly."
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"_schema": {
|
||||
"description": "Manual-action manifest for self-hosted upgrades, read by update.sh with jq. Hand-edit this file directly; it is the source of truth (no generation step). Keys are self-hosted release versions as bare semver (e.g. \"0.7.0\"), matching the self-hosted/vX.Y.Z tag and the \"## [0.7.0]\" CHANGELOG heading. Only add an entry for a release that needs an action a file diff cannot encode: a data migration, a run-this-first script, or a breaking default. Routine config changes are applied automatically by update.sh's 3-way merge and must NOT be listed here. update.sh gates on entries in (your version, target version], ordered with sort -V.",
|
||||
"fields": {
|
||||
"breaking": "bool - requires explicit user confirmation before applying",
|
||||
"gate": "string|null - script to run before upgrading past this release (e.g. utils/upgrade-pg17.sh)",
|
||||
"migration_guide_url": "string|null - link to the per-release migration guide",
|
||||
"requires": "string[] - free-text manual steps shown to the user"
|
||||
}
|
||||
},
|
||||
"0.6.0": {
|
||||
"breaking": true,
|
||||
"gate": "utils/upgrade-pg17.sh",
|
||||
"migration_guide_url": "https://supabase.com/docs/guides/self-hosting/postgres-upgrade-17",
|
||||
"requires": [
|
||||
"Postgres 17 is now the default. Do NOT start Postgres 17 against an existing Postgres 15 data directory - back up your database first.",
|
||||
"Run 'sudo bash utils/upgrade-pg17.sh' (needs bash + root) to migrate Postgres 15 -> 17, then recreate containers. To defer, pin Postgres 15 with the docker-compose.pg15.yml override.",
|
||||
"Includes a security fix for the API gateway (Realtime /api/tenants and /api/openapi routes) - strongly recommended for any instance running Realtime."
|
||||
]
|
||||
},
|
||||
"0.7.0": {
|
||||
"breaking": true,
|
||||
"gate": null,
|
||||
"migration_guide_url": "https://github.com/orgs/supabase/discussions/47093",
|
||||
"requires": [
|
||||
"API_EXTERNAL_URL now includes the /auth/v1 path prefix, and SAML SSO endpoints moved to /auth/v1/sso/saml/*. Update custom OAuth provider callback URLs and any SAML configuration accordingly.",
|
||||
"Anon (publishable) key access to the OpenAPI spec at /rest/v1/ has been removed. Use the service role or secret API key if you relied on it; normal data access via /rest/v1/<table> is unaffected."
|
||||
]
|
||||
},
|
||||
"0.8.0": {
|
||||
"breaking": true,
|
||||
"gate": null,
|
||||
"migration_guide_url": "https://github.com/orgs/supabase/discussions/48048",
|
||||
"requires": [
|
||||
"Envoy is now the default API gateway, replacing Kong. The gateway service is renamed from 'kong' to 'api-gw' (container 'supabase-envoy'); the 'kong' network alias still resolves, so internal service references keep working.",
|
||||
"If you customized volumes/api/kong.yml or the gateway service, enable the Kong override with 'sh run.sh config add kong' to keep running Kong; otherwise the merge switches you to Envoy."
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Add asymmetric key pair and opaque API keys to a self-hosted Supabase installation.
|
||||
#
|
||||
# Reads JWT_SECRET from .env and generates:
|
||||
# - EC P-256 key pair (JWT_KEYS, JWT_JWKS)
|
||||
# - Opaque API keys (SUPABASE_PUBLISHABLE_KEY, SUPABASE_SECRET_KEY)
|
||||
# - Internal: ES256 JWT API keys (ANON_KEY_ASYMMETRIC, SERVICE_ROLE_KEY_ASYMMETRIC)
|
||||
#
|
||||
# Usage:
|
||||
# sh add-new-auth-keys.sh # Interactive: prints keys, prompts to update .env
|
||||
# sh add-new-auth-keys.sh --update-env # Prints keys and writes them to .env
|
||||
# sh add-new-auth-keys.sh | tee keys # Non-interactive: prints keys only
|
||||
#
|
||||
# Prerequisites:
|
||||
# - .env file with JWT_SECRET set (run generate-keys.sh first)
|
||||
# - node (>= 16) or docker
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
node_ok() {
|
||||
command -v node >/dev/null 2>&1 || return 1
|
||||
major=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
||||
[ -n "$major" ] && [ "$major" -ge 16 ] 2>/dev/null
|
||||
}
|
||||
|
||||
# Resolve how to run node: local install (>= 16) preferred, docker fallback.
|
||||
if node_ok; then
|
||||
node_runner="node"
|
||||
else
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
echo "Local node $(node -v) is too old (need >= 16), falling back to docker."
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: requires either node (>= 16) or docker."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "Error: docker is installed but the daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker image inspect node:22-alpine >/dev/null 2>&1; then
|
||||
echo "Pulling node:22-alpine (first-run only)..."
|
||||
docker pull node:22-alpine
|
||||
fi
|
||||
|
||||
node_runner="docker run --rm node:22-alpine node"
|
||||
fi
|
||||
|
||||
# Read JWT_SECRET from .env
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run generate-keys.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jwt_secret=$(grep '^JWT_SECRET=' .env | cut -d= -f2- | tr -d '\r')
|
||||
if [ -z "$jwt_secret" ]; then
|
||||
echo "Error: JWT_SECRET not found in .env. Run generate-keys.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
# Node.js does the crypto-heavy work:
|
||||
# - EC P-256 keypair generation
|
||||
# - JWKS construction (with symmetric key included)
|
||||
# - ES256 JWT signing
|
||||
# - Opaque API key generation with checksum
|
||||
$node_runner -e '
|
||||
const crypto = require("crypto");
|
||||
|
||||
const jwtSecret = process.argv[1];
|
||||
|
||||
// Generate EC P-256 keypair and export as JWK
|
||||
const { privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
||||
const jwkPrivate = privateKey.export({ format: "jwk" });
|
||||
|
||||
const kid = crypto.randomUUID();
|
||||
|
||||
// Symmetric key as JWK (base64url-encoded)
|
||||
const octKey = {
|
||||
kty: "oct",
|
||||
k: Buffer.from(jwtSecret).toString("base64url"),
|
||||
alg: "HS256"
|
||||
};
|
||||
|
||||
// JWKS with private key (for Auth to sign tokens)
|
||||
const jwksKeypair = { keys: [
|
||||
{ kty: "EC", kid, use: "sig", key_ops: ["sign", "verify"], alg: "ES256", ext: true,
|
||||
crv: jwkPrivate.crv, x: jwkPrivate.x, y: jwkPrivate.y, d: jwkPrivate.d },
|
||||
octKey
|
||||
]};
|
||||
|
||||
// JWKS with public key only (for PostgREST, Realtime, Storage to verify)
|
||||
const jwksPublic = { keys: [
|
||||
{ kty: "EC", kid, use: "sig", key_ops: ["verify"], alg: "ES256", ext: true,
|
||||
crv: jwkPrivate.crv, x: jwkPrivate.x, y: jwkPrivate.y },
|
||||
octKey
|
||||
]};
|
||||
|
||||
// Sign ES256 JWT
|
||||
function signES256(payload) {
|
||||
const header = { alg: "ES256", typ: "JWT", kid };
|
||||
const b64Header = Buffer.from(JSON.stringify(header)).toString("base64url");
|
||||
const b64Payload = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
||||
const data = b64Header + "." + b64Payload;
|
||||
const sig = crypto.sign("SHA256", Buffer.from(data), {
|
||||
key: privateKey,
|
||||
dsaEncoding: "ieee-p1363"
|
||||
}).toString("base64url");
|
||||
return data + "." + sig;
|
||||
}
|
||||
|
||||
const iat = Math.floor(Date.now() / 1000);
|
||||
const exp = iat + 5 * 365 * 24 * 3600; // 5 years
|
||||
|
||||
const anonJwt = signES256({ role: "anon", iss: "supabase", iat, exp });
|
||||
const serviceJwt = signES256({ role: "service_role", iss: "supabase", iat, exp });
|
||||
|
||||
// Generate opaque API keys with checksum
|
||||
const PROJECT_REF = "supabase-self-hosted";
|
||||
|
||||
function generateOpaqueKey(prefix) {
|
||||
const random = crypto.randomBytes(17).toString("base64url").slice(0, 22);
|
||||
const intermediate = prefix + random;
|
||||
const checksum = crypto.createHash("sha256")
|
||||
.update(PROJECT_REF + "|" + intermediate)
|
||||
.digest("base64url")
|
||||
.slice(0, 8);
|
||||
return intermediate + "_" + checksum;
|
||||
}
|
||||
|
||||
const publishableKey = generateOpaqueKey("sb_publishable_");
|
||||
const secretKey = generateOpaqueKey("sb_secret_");
|
||||
|
||||
// Output as KEY=value lines for shell to parse
|
||||
console.log("SUPABASE_PUBLISHABLE_KEY=" + publishableKey);
|
||||
console.log("SUPABASE_SECRET_KEY=" + secretKey);
|
||||
console.log("ANON_KEY_ASYMMETRIC=" + anonJwt);
|
||||
console.log("SERVICE_ROLE_KEY_ASYMMETRIC=" + serviceJwt);
|
||||
console.log("JWT_KEYS=" + JSON.stringify(jwksKeypair.keys));
|
||||
console.log("JWT_JWKS=" + JSON.stringify(jwksPublic));
|
||||
' "$jwt_secret" > "$tmpdir/output"
|
||||
|
||||
# Read generated values
|
||||
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
ANON_KEY_ASYMMETRIC=$(grep '^ANON_KEY_ASYMMETRIC=' "$tmpdir/output" | cut -d= -f2-)
|
||||
SERVICE_ROLE_KEY_ASYMMETRIC=$(grep '^SERVICE_ROLE_KEY_ASYMMETRIC=' "$tmpdir/output" | cut -d= -f2-)
|
||||
JWT_KEYS=$(grep '^JWT_KEYS=' "$tmpdir/output" | cut -d= -f2-)
|
||||
JWT_JWKS=$(grep '^JWT_JWKS=' "$tmpdir/output" | cut -d= -f2-)
|
||||
|
||||
echo ""
|
||||
echo "SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY}"
|
||||
echo "SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY}"
|
||||
echo ""
|
||||
echo "JWT_KEYS=${JWT_KEYS}"
|
||||
echo ""
|
||||
echo "JWT_JWKS=${JWT_JWKS}"
|
||||
echo ""
|
||||
echo "Ensure the following configuration is uncommented in docker-compose.yml for the asymmetric key pair to work:"
|
||||
echo ""
|
||||
echo " Auth: GOTRUE_JWT_KEYS: \${JWT_KEYS:-[]}"
|
||||
echo " Realtime: API_JWT_JWKS: \${JWT_JWKS:-{\"keys\":[]}}"
|
||||
echo " Storage: JWT_JWKS: \${JWT_JWKS:-{\"keys\":[]}}"
|
||||
echo " Functions: SUPABASE_JWKS: \${JWT_JWKS:-{\"keys\":[]}}"
|
||||
echo ""
|
||||
|
||||
if [ "$1" = "--update-env" ]; then
|
||||
update_env=true
|
||||
elif test -t 0; then
|
||||
printf "Update .env file? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy]) update_env=true ;;
|
||||
*) update_env=false ;;
|
||||
esac
|
||||
else
|
||||
echo "Running non-interactively. Pass --update-env to write to .env."
|
||||
update_env=false
|
||||
fi
|
||||
|
||||
if [ "$update_env" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Updating .env..."
|
||||
|
||||
# Append new variables if they don't exist, or update them if they do
|
||||
for var in SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY ANON_KEY_ASYMMETRIC SERVICE_ROLE_KEY_ASYMMETRIC JWT_KEYS JWT_JWKS; do
|
||||
eval "val=\$$var"
|
||||
if grep -q "^${var}=" .env; then
|
||||
sed -i.old -e "s|^${var}=.*$|${var}=${val}|" .env
|
||||
else
|
||||
echo "${var}=${val}" >> .env
|
||||
fi
|
||||
done
|
||||
|
||||
# Uncomment new auth configuration in docker-compose.yml
|
||||
echo "Updating docker-compose.yml..."
|
||||
if [ ! -f docker-compose.yml ]; then
|
||||
echo "Error: docker-compose.yml not found in $(pwd)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Always fall through to the grep check
|
||||
sed -i.old \
|
||||
-e '/^[ ]*#GOTRUE_JWT_KEYS:/ s/#//' \
|
||||
-e '/^[ ]*#API_JWT_JWKS:/ s/#//' \
|
||||
-e '/^[ ]*#JWT_JWKS:/ s/#//' \
|
||||
-e '/^[ ]*#SUPABASE_JWKS:/ s/#//' \
|
||||
docker-compose.yml || true
|
||||
|
||||
if grep -q '^[ ]*GOTRUE_JWT_KEYS:' docker-compose.yml && \
|
||||
grep -q '^[ ]*API_JWT_JWKS:' docker-compose.yml && \
|
||||
grep -q '^[ ]*JWT_JWKS:' docker-compose.yml && \
|
||||
grep -q '^[ ]*SUPABASE_JWKS:' docker-compose.yml; then
|
||||
echo "Done."
|
||||
else
|
||||
echo "Warning: could not edit docker-compose.yml. Uncomment auth configuration manually."
|
||||
fi
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Portions of this code are derived from Inder Singh's update-db-pass.sh
|
||||
# Copyright 2025 Inder Singh. Licensed under Apache License 2.0.
|
||||
# Original source:
|
||||
# https://github.com/singh-inder/supabase-automated-self-host/blob/main/docker/update-db-pass.sh
|
||||
#
|
||||
# GitHub discussion here:
|
||||
# https://github.com/supabase/supabase/issues/22605#issuecomment-3323382144
|
||||
#
|
||||
# Changed:
|
||||
# - POSIX shell compatibility
|
||||
# - No hardcoded values for database service and admin user
|
||||
# - Use .env for the admin user and database service port
|
||||
# - Does _not_ set password for supabase_read_only_user (this role is not
|
||||
# supposed to have a password)
|
||||
# - Print all values and confirm before updating
|
||||
# - Stop on any errors
|
||||
#
|
||||
# Heads up:
|
||||
# - Updating _analytics.source_backends is not needed after PR logflare#2069
|
||||
# - Newer Logflare versions use a different table and update connection string
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
if ! docker compose version > /dev/null 2>&1; then
|
||||
echo "Docker Compose not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Missing .env file. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate random hex-only password to avoid issues with SQL/shell
|
||||
new_passwd="$(openssl rand -hex 16)"
|
||||
# If replacing with a custom password, avoid using @/?#:&
|
||||
# https://supabase.com/docs/guides/database/postgres/roles#passwords
|
||||
# new_passwd="d0notUseSpecialSymbolsForPq123-"
|
||||
|
||||
# Check Postgres service
|
||||
db_image_prefix="supabase.postgres:"
|
||||
|
||||
compose_output=$(docker compose ps \
|
||||
--format '{{.Image}}\t{{.Service}}\t{{.Status}}' 2>/dev/null | \
|
||||
grep -m1 "^$db_image_prefix" || true)
|
||||
|
||||
if [ -z "$compose_output" ]; then
|
||||
echo "Postgres container not found. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
db_image=$(echo "$compose_output" | cut -f1)
|
||||
db_srv_name=$(echo "$compose_output" | cut -f2)
|
||||
db_srv_status=$(echo "$compose_output" | cut -f3)
|
||||
|
||||
case "$db_srv_status" in
|
||||
Up*)
|
||||
;;
|
||||
*)
|
||||
echo "Postgres container status: $db_srv_status"
|
||||
echo "Exiting."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
db_srv_port=$(grep "^POSTGRES_PORT=" .env | cut -d '=' -f 2)
|
||||
port_source=" (.env):"
|
||||
if [ -z "$db_srv_port" ]; then
|
||||
db_srv_port="5432"
|
||||
port_source=" (default):"
|
||||
fi
|
||||
|
||||
db_admin_user="supabase_admin"
|
||||
|
||||
echo ""
|
||||
echo "*** Check configuration below before updating database passwords! ***"
|
||||
echo ""
|
||||
echo "Service name: $db_srv_name"
|
||||
echo "Service status: $db_srv_status"
|
||||
echo "Service port${port_source} $db_srv_port"
|
||||
echo "Image: $db_image"
|
||||
echo ""
|
||||
echo "Admin user: $db_admin_user"
|
||||
|
||||
if ! test -t 0; then
|
||||
echo ""
|
||||
echo "Running non-interactively. Not updating passwords."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "New database password: $new_passwd"
|
||||
echo ""
|
||||
|
||||
printf "Update database passwords? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy])
|
||||
;;
|
||||
*)
|
||||
echo "Canceled. Not updating passwords."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Updating passwords..."
|
||||
echo "Connecting to the database service container..."
|
||||
|
||||
docker compose exec -T "$db_srv_name" psql -U "$db_admin_user" -d "_supabase" -v ON_ERROR_STOP=1 <<EOF
|
||||
alter user anon with password '${new_passwd}';
|
||||
alter user authenticated with password '${new_passwd}';
|
||||
alter user authenticator with password '${new_passwd}';
|
||||
alter user dashboard_user with password '${new_passwd}';
|
||||
alter user pgbouncer with password '${new_passwd}';
|
||||
alter user postgres with password '${new_passwd}';
|
||||
alter user service_role with password '${new_passwd}';
|
||||
alter user supabase_admin with password '${new_passwd}';
|
||||
alter user supabase_auth_admin with password '${new_passwd}';
|
||||
alter user supabase_functions_admin with password '${new_passwd}';
|
||||
alter user supabase_replication_admin with password '${new_passwd}';
|
||||
alter user supabase_storage_admin with password '${new_passwd}';
|
||||
|
||||
DROP SCHEMA _supavisor CASCADE;
|
||||
create schema if not exists _supavisor;
|
||||
alter schema _supavisor owner to supabase_admin;
|
||||
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = '_analytics'
|
||||
AND table_name = 'source_backends'
|
||||
) THEN
|
||||
UPDATE _analytics.source_backends
|
||||
SET config = jsonb_set(
|
||||
config,
|
||||
'{url}',
|
||||
'"postgresql://${db_admin_user}:${new_passwd}@${db_srv_name}:${db_srv_port}/postgres"',
|
||||
false
|
||||
)
|
||||
WHERE type = 'postgres';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
EOF
|
||||
|
||||
echo "Updating POSTGRES_PASSWORD in .env..."
|
||||
sed -i.old "s|^POSTGRES_PASSWORD=.*$|POSTGRES_PASSWORD=$new_passwd|" .env
|
||||
|
||||
echo ""
|
||||
echo "Success. To update and restart containers use:"
|
||||
echo ""
|
||||
echo "docker compose up -d --force-recreate"
|
||||
echo ""
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Generate secrets and legacy symmetric JWT API keys for self-hosted Supabase.
|
||||
#
|
||||
# Generates: JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY, and other secrets
|
||||
# needed for a fresh installation.
|
||||
#
|
||||
# Usage:
|
||||
# sh generate-keys.sh # Interactive: prints keys, prompts to update .env
|
||||
# sh generate-keys.sh --update-env # Prints keys and writes them to .env
|
||||
# sh generate-keys.sh | tee keys # Non-interactive: prints keys only
|
||||
#
|
||||
# Portions of this code are derived from Inder Singh's setup.sh shell script.
|
||||
# Copyright 2025 Inder Singh. Licensed under Apache License 2.0.
|
||||
# Original source: https://github.com/singh-inder/supabase-automated-self-host/blob/main/setup.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
gen_hex() {
|
||||
openssl rand -hex "$1"
|
||||
}
|
||||
|
||||
gen_base64() {
|
||||
openssl rand -base64 "$1"
|
||||
}
|
||||
|
||||
base64_url_encode() {
|
||||
openssl enc -base64 -A | tr '+/' '-_' | tr -d '='
|
||||
}
|
||||
|
||||
gen_token() {
|
||||
payload=$1
|
||||
payload_base64=$(printf %s "$payload" | base64_url_encode)
|
||||
header_base64=$(printf %s "$header" | base64_url_encode)
|
||||
signed_content="${header_base64}.${payload_base64}"
|
||||
signature=$(printf %s "$signed_content" | openssl dgst -binary -sha256 -hmac "$jwt_secret" | base64_url_encode)
|
||||
printf '%s' "${signed_content}.${signature}"
|
||||
}
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
echo "Error: openssl is required but not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jwt_secret="$(gen_base64 30)"
|
||||
|
||||
# Used in gen_token()
|
||||
header='{"alg":"HS256","typ":"JWT"}'
|
||||
iat=$(date +%s)
|
||||
exp=$((iat + 5 * 3600 * 24 * 365)) # 5 years
|
||||
|
||||
# Normalizes JSON formatting so that the token matches https://www.jwt.io/ results
|
||||
anon_payload="{\"role\":\"anon\",\"iss\":\"supabase\",\"iat\":$iat,\"exp\":$exp}"
|
||||
service_role_payload="{\"role\":\"service_role\",\"iss\":\"supabase\",\"iat\":$iat,\"exp\":$exp}"
|
||||
|
||||
#echo "anon_payload=$anon_payload"
|
||||
#echo "service_role_payload=$service_role_payload"
|
||||
|
||||
anon_key=$(gen_token "$anon_payload")
|
||||
service_role_key=$(gen_token "$service_role_payload")
|
||||
|
||||
secret_key_base=$(gen_base64 48)
|
||||
realtime_db_enc_key=$(gen_hex 8)
|
||||
vault_enc_key=$(gen_hex 16)
|
||||
pg_meta_crypto_key=$(gen_base64 24)
|
||||
|
||||
logflare_public_access_token=$(gen_base64 24)
|
||||
logflare_private_access_token=$(gen_base64 24)
|
||||
|
||||
s3_protocol_access_key_id=$(gen_hex 16)
|
||||
s3_protocol_access_key_secret=$(gen_hex 32)
|
||||
|
||||
minio_root_password=$(gen_hex 16)
|
||||
|
||||
echo ""
|
||||
echo "JWT_SECRET=${jwt_secret}"
|
||||
echo ""
|
||||
#echo "Issued at: $iat"
|
||||
#echo "Expire: $exp"
|
||||
echo "ANON_KEY=${anon_key}"
|
||||
echo "SERVICE_ROLE_KEY=${service_role_key}"
|
||||
echo ""
|
||||
echo "SECRET_KEY_BASE=${secret_key_base}"
|
||||
echo "REALTIME_DB_ENC_KEY=${realtime_db_enc_key}"
|
||||
echo "VAULT_ENC_KEY=${vault_enc_key}"
|
||||
echo "PG_META_CRYPTO_KEY=${pg_meta_crypto_key}"
|
||||
echo "LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}"
|
||||
echo "LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}"
|
||||
echo "S3_PROTOCOL_ACCESS_KEY_ID=${s3_protocol_access_key_id}"
|
||||
echo "S3_PROTOCOL_ACCESS_KEY_SECRET=${s3_protocol_access_key_secret}"
|
||||
echo "MINIO_ROOT_PASSWORD=${minio_root_password}"
|
||||
echo ""
|
||||
|
||||
postgres_password=$(gen_hex 16)
|
||||
dashboard_password=$(gen_hex 16)
|
||||
|
||||
echo "POSTGRES_PASSWORD=${postgres_password}"
|
||||
echo "DASHBOARD_PASSWORD=${dashboard_password}"
|
||||
echo ""
|
||||
|
||||
if [ "$1" = "--update-env" ]; then
|
||||
update_env=true
|
||||
elif test -t 0; then
|
||||
printf "Update .env file? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy]) update_env=true ;;
|
||||
*) update_env=false ;;
|
||||
esac
|
||||
else
|
||||
echo "Running non-interactively. Pass --update-env to write to .env."
|
||||
update_env=false
|
||||
fi
|
||||
|
||||
if [ "$update_env" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Updating .env..."
|
||||
|
||||
sed \
|
||||
-i.old \
|
||||
-e "s|^JWT_SECRET=.*$|JWT_SECRET=${jwt_secret}|" \
|
||||
-e "s|^ANON_KEY=.*$|ANON_KEY=${anon_key}|" \
|
||||
-e "s|^SERVICE_ROLE_KEY=.*$|SERVICE_ROLE_KEY=${service_role_key}|" \
|
||||
-e "s|^SECRET_KEY_BASE=.*$|SECRET_KEY_BASE=${secret_key_base}|" \
|
||||
-e "s|^REALTIME_DB_ENC_KEY=.*$|REALTIME_DB_ENC_KEY=${realtime_db_enc_key}|" \
|
||||
-e "s|^VAULT_ENC_KEY=.*$|VAULT_ENC_KEY=${vault_enc_key}|" \
|
||||
-e "s|^PG_META_CRYPTO_KEY=.*$|PG_META_CRYPTO_KEY=${pg_meta_crypto_key}|" \
|
||||
-e "s|^LOGFLARE_PUBLIC_ACCESS_TOKEN=.*$|LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}|" \
|
||||
-e "s|^LOGFLARE_PRIVATE_ACCESS_TOKEN=.*$|LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}|" \
|
||||
-e "s|^S3_PROTOCOL_ACCESS_KEY_ID=.*$|S3_PROTOCOL_ACCESS_KEY_ID=${s3_protocol_access_key_id}|" \
|
||||
-e "s|^S3_PROTOCOL_ACCESS_KEY_SECRET=.*$|S3_PROTOCOL_ACCESS_KEY_SECRET=${s3_protocol_access_key_secret}|" \
|
||||
-e "s|^MINIO_ROOT_PASSWORD=.*$|MINIO_ROOT_PASSWORD=${minio_root_password}|" \
|
||||
-e "s|^POSTGRES_PASSWORD=.*$|POSTGRES_PASSWORD=${postgres_password}|" \
|
||||
-e "s|^DASHBOARD_PASSWORD=.*$|DASHBOARD_PASSWORD=${dashboard_password}|" \
|
||||
.env
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Reassign ownership of public schema objects from supabase_admin to postgres.
|
||||
#
|
||||
# Context and documentation:
|
||||
# https://supabase.com/docs/guides/self-hosting/remove-superuser-access
|
||||
#
|
||||
# Credits:
|
||||
# Original version by Inder Singh.
|
||||
#
|
||||
# Usage:
|
||||
# sh utils/reassign-owner.sh
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Docker Compose not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Postgres service
|
||||
db_image_prefix="supabase.postgres:"
|
||||
|
||||
compose_output=$(docker compose ps \
|
||||
--format '{{.Image}}\t{{.Service}}\t{{.Status}}' 2>/dev/null |
|
||||
grep -m1 "^$db_image_prefix" || true)
|
||||
|
||||
if [ -z "$compose_output" ]; then
|
||||
echo "Postgres container not found. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
db_srv_name=$(echo "$compose_output" | cut -f2)
|
||||
db_srv_status=$(echo "$compose_output" | cut -f3)
|
||||
|
||||
case "$db_srv_status" in
|
||||
Up*)
|
||||
;;
|
||||
*)
|
||||
echo "Postgres container status: $db_srv_status"
|
||||
echo "Exiting."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! test -t 0; then
|
||||
echo ""
|
||||
echo "Running non-interactively. Not reassigning ownership."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf "Reassign public schema objects to postgres user? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy])
|
||||
;;
|
||||
*)
|
||||
echo "Canceled. Not reassigning ownership."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
docker compose exec -T "$db_srv_name" psql -v ON_ERROR_STOP=1 -U supabase_admin -d postgres <<'EOF'
|
||||
\echo 'Current supabase_admin-owned objects in public schema:'
|
||||
SELECT c.relname, c.relkind, c.relowner::regrole
|
||||
FROM pg_class c
|
||||
WHERE c.relnamespace = 'public'::regnamespace
|
||||
AND c.relowner = 'supabase_admin'::regrole;
|
||||
|
||||
-- Reassign user objects in public schema from supabase_admin to postgres.
|
||||
-- (Only affects public schema; Supabase-managed schemas stay as-is.
|
||||
-- Extension-owned objects are skipped.)
|
||||
DO $$
|
||||
DECLARE
|
||||
rec record;
|
||||
rel_count int := 0;
|
||||
fn_count int := 0;
|
||||
type_count int := 0;
|
||||
BEGIN
|
||||
-- Tables, views, sequences, materialized views, partitioned tables
|
||||
FOR rec IN
|
||||
SELECT c.relname, c.relkind
|
||||
FROM pg_class c
|
||||
WHERE c.relnamespace = 'public'::regnamespace
|
||||
AND c.relowner = 'supabase_admin'::regrole
|
||||
AND c.relkind IN ('r', 'v', 'S', 'm', 'p')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_depend d
|
||||
WHERE d.classid = 'pg_class'::regclass
|
||||
AND d.objid = c.oid
|
||||
AND d.deptype = 'e'
|
||||
)
|
||||
ORDER BY CASE c.relkind
|
||||
WHEN 'p' THEN 0 -- partitioned parents first; cascades ownership to partitions
|
||||
WHEN 'm' THEN 1
|
||||
WHEN 'r' THEN 2
|
||||
WHEN 'v' THEN 3
|
||||
WHEN 'S' THEN 4
|
||||
END
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE public.%I OWNER TO postgres', rec.relname);
|
||||
rel_count := rel_count + 1;
|
||||
END LOOP;
|
||||
|
||||
-- Functions and procedures
|
||||
FOR rec IN
|
||||
SELECT p.oid, p.proname, pg_get_function_identity_arguments(p.oid) AS args
|
||||
FROM pg_proc p
|
||||
WHERE p.pronamespace = 'public'::regnamespace
|
||||
AND p.proowner = 'supabase_admin'::regrole
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_depend d
|
||||
WHERE d.classid = 'pg_proc'::regclass
|
||||
AND d.objid = p.oid
|
||||
AND d.deptype = 'e'
|
||||
)
|
||||
LOOP
|
||||
EXECUTE format('ALTER ROUTINE public.%I(%s) OWNER TO postgres', rec.proname, rec.args);
|
||||
fn_count := fn_count + 1;
|
||||
END LOOP;
|
||||
|
||||
-- Types (excluding array types and table-bound composites)
|
||||
FOR rec IN
|
||||
SELECT t.typname
|
||||
FROM pg_type t
|
||||
WHERE t.typnamespace = 'public'::regnamespace
|
||||
AND t.typowner = 'supabase_admin'::regrole
|
||||
AND t.typrelid = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_type el
|
||||
WHERE el.oid = t.typelem
|
||||
AND el.typarray = t.oid
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM pg_depend d
|
||||
WHERE d.classid = 'pg_type'::regclass
|
||||
AND d.objid = t.oid
|
||||
AND d.deptype = 'e'
|
||||
)
|
||||
LOOP
|
||||
EXECUTE format('ALTER TYPE public.%I OWNER TO postgres', rec.typname);
|
||||
type_count := type_count + 1;
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE 'Reassigned % relation(s), % routine(s), % type(s) from supabase_admin to postgres.',
|
||||
rel_count, fn_count, type_count;
|
||||
END
|
||||
$$;
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Rotate opaque API keys for a self-hosted Supabase installation.
|
||||
#
|
||||
# Regenerates SUPABASE_PUBLISHABLE_KEY and SUPABASE_SECRET_KEY
|
||||
# without touching the asymmetric key pair (JWKS) or JWT tokens.
|
||||
#
|
||||
# Usage:
|
||||
# sh rotate-new-api-keys.sh # Interactive: prints keys, prompts to update .env
|
||||
# sh rotate-new-api-keys.sh --update-env # Prints keys and writes them to .env
|
||||
# sh rotate-new-api-keys.sh | tee keys # Non-interactive: prints keys only
|
||||
#
|
||||
# Prerequisites:
|
||||
# - .env file (run generate-keys.sh and add-new-auth-keys.sh first)
|
||||
# - node (>= 16) or docker
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
node_ok() {
|
||||
command -v node >/dev/null 2>&1 || return 1
|
||||
major=$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)
|
||||
[ -n "$major" ] && [ "$major" -ge 16 ] 2>/dev/null
|
||||
}
|
||||
|
||||
# Resolve how to run node: local install (>= 16) preferred, docker fallback.
|
||||
if node_ok; then
|
||||
node_runner="node"
|
||||
else
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
echo "Local node $(node -v) is too old (need >= 16), falling back to docker."
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: requires either node (>= 16) or docker."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
echo "Error: docker is installed but the daemon is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker image inspect node:22-alpine >/dev/null 2>&1; then
|
||||
echo "Pulling node:22-alpine (first-run only)..."
|
||||
docker pull node:22-alpine
|
||||
fi
|
||||
|
||||
node_runner="docker run --rm node:22-alpine node"
|
||||
fi
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "Error: .env file not found. Run generate-keys.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
$node_runner -e '
|
||||
const crypto = require("crypto");
|
||||
|
||||
const PROJECT_REF = "supabase-self-hosted";
|
||||
|
||||
function generateOpaqueKey(prefix) {
|
||||
const random = crypto.randomBytes(17).toString("base64url").slice(0, 22);
|
||||
const intermediate = prefix + random;
|
||||
const checksum = crypto.createHash("sha256")
|
||||
.update(PROJECT_REF + "|" + intermediate)
|
||||
.digest("base64url")
|
||||
.slice(0, 8);
|
||||
return intermediate + "_" + checksum;
|
||||
}
|
||||
|
||||
const publishableKey = generateOpaqueKey("sb_publishable_");
|
||||
const secretKey = generateOpaqueKey("sb_secret_");
|
||||
|
||||
console.log("SUPABASE_PUBLISHABLE_KEY=" + publishableKey);
|
||||
console.log("SUPABASE_SECRET_KEY=" + secretKey);
|
||||
' > "$tmpdir/output"
|
||||
|
||||
SUPABASE_PUBLISHABLE_KEY=$(grep '^SUPABASE_PUBLISHABLE_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
SUPABASE_SECRET_KEY=$(grep '^SUPABASE_SECRET_KEY=' "$tmpdir/output" | cut -d= -f2-)
|
||||
|
||||
echo ""
|
||||
echo "SUPABASE_PUBLISHABLE_KEY=${SUPABASE_PUBLISHABLE_KEY}"
|
||||
echo "SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY}"
|
||||
echo ""
|
||||
|
||||
if [ "$1" = "--update-env" ]; then
|
||||
update_env=true
|
||||
elif test -t 0; then
|
||||
printf "Update .env file? (y/N) "
|
||||
read -r REPLY
|
||||
case "$REPLY" in
|
||||
[Yy]) update_env=true ;;
|
||||
*) update_env=false ;;
|
||||
esac
|
||||
else
|
||||
echo "Running non-interactively. Pass --update-env to write to .env."
|
||||
update_env=false
|
||||
fi
|
||||
|
||||
if [ "$update_env" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Updating .env..."
|
||||
|
||||
for var in SUPABASE_PUBLISHABLE_KEY SUPABASE_SECRET_KEY; do
|
||||
eval "val=\$$var"
|
||||
if grep -q "^${var}=" .env; then
|
||||
sed -i.old -e "s|^${var}=.*$|${var}=${val}|" .env
|
||||
else
|
||||
echo "${var}=${val}" >> .env
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,822 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Requires bash (not sh) for pipefail, which ensures failures in piped
|
||||
# commands are caught during the upgrade.
|
||||
#
|
||||
# Upgrade self-hosted Supabase Postgres from 15 to 17.
|
||||
#
|
||||
# Uses Supabase's pg_upgrade scripts (initiate.sh + complete.sh) inside a
|
||||
# temporary PG 15 container, then swaps data directories and starts Postgres 17.
|
||||
#
|
||||
# Usage (must be run as root or with sudo):
|
||||
# cd docker/
|
||||
# sudo bash utils/upgrade-pg17.sh # Interactive (prompts for confirmation)
|
||||
# sudo bash utils/upgrade-pg17.sh --yes # Non-interactive (skip all prompts)
|
||||
#
|
||||
# Requirements:
|
||||
# - Docker with Docker Compose (docker compose, not docker-compose)
|
||||
# - Running Supabase self-hosted setup with Postgres 15
|
||||
# - At least 2x current database size + 5 GB free disk space
|
||||
#
|
||||
# Backup:
|
||||
# The original Postgres 15 data directory is preserved as
|
||||
# ./volumes/db/data.bak.pg15 during the upgrade.
|
||||
# DO NOT DELETE it until you have verified the upgrade was successful.
|
||||
#
|
||||
# Rollback (if the upgrade fails or you want to revert):
|
||||
# 1. docker compose -f docker-compose.yml -f docker-compose.pg17.yml down
|
||||
# 2. rm -rf ./volumes/db/data
|
||||
# 3. mv ./volumes/db/data.bak.pg15 ./volumes/db/data
|
||||
# 4. docker compose -f docker-compose.yml -f docker-compose.pg15.yml run --rm db chown -R postgres:postgres /etc/postgresql-custom/
|
||||
# 5. docker compose -f docker-compose.yml -f docker-compose.pg15.yml up -d
|
||||
#
|
||||
|
||||
# Ensure we're running under bash (not sh/zsh/dash).
|
||||
# Check that $BASH ends with /bash (not /sh, /zsh, etc.).
|
||||
case "${BASH:-}" in
|
||||
*/bash) ;;
|
||||
*) echo "Error: This script requires bash. Run it with: sudo bash $0" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
AUTO_CONFIRM=false
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--yes|-y) AUTO_CONFIRM=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Configuration ----------------------------------------------------------
|
||||
|
||||
# Image used for the upgrade tarball + complete.sh container.
|
||||
# Must share glibc with PG 15 (the extracted ELF binaries run inside PG15).
|
||||
# Pinned to .063: later images bumped glibc, which breaks the ELF extraction.
|
||||
PG17_UPGRADE_IMAGE="supabase/postgres:17.6.1.063"
|
||||
# Tag in supabase/postgres repo matching the upgrade image (for downloading scripts)
|
||||
PG17_SCRIPTS_REF="17.6.1.063"
|
||||
|
||||
# Final Postgres 17 image that runs after the upgrade. Pinned here (not read from
|
||||
# the compose files). Keep in sync with the db image in docker-compose.yml (and
|
||||
# docker-compose.pg17.yml). Used for pulling, chowning data/db-config to the
|
||||
# target's postgres UID, and running the post-upgrade migrations.
|
||||
PG17_TARGET_IMAGE="supabase/postgres:17.6.1.136"
|
||||
|
||||
DB_CONTAINER="supabase-db"
|
||||
UPGRADE_CONTAINER="supabase-pg-upgrade"
|
||||
COMPLETE_CONTAINER="supabase-pg-complete"
|
||||
|
||||
DATA_DIR="./volumes/db/data"
|
||||
BACKUP_DIR="./volumes/db/data.bak.pg15"
|
||||
# Include image tag in cache filename so changing PG17_UPGRADE_IMAGE invalidates it
|
||||
PG17_TAG="${PG17_UPGRADE_IMAGE##*:}"
|
||||
TARBALL_CACHE="./volumes/db/pg17_upgrade_bin_${PG17_TAG}.tar.gz"
|
||||
# initiate.sh writes pg_upgrade output here: pgdata/, conf/, sql/
|
||||
MIGRATION_DIR="./volumes/db/data_migration"
|
||||
|
||||
# --- Helpers ----------------------------------------------------------------
|
||||
|
||||
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
|
||||
info() { printf '\n==> %s\n' "$*"; }
|
||||
warn() { printf 'Warning: %s\n' "$*" >&2; }
|
||||
|
||||
# Temp dir on host for tarball + scripts (mounted into containers)
|
||||
staging_dir=""
|
||||
pg_password=""
|
||||
current_image=""
|
||||
drop_extensions=""
|
||||
db_config_vol=""
|
||||
|
||||
# Remove leftover containers and staging dir on exit.
|
||||
# Uses an alpine container for rm because the tarball build runs as root
|
||||
# inside Docker - the resulting files are root-owned and can't be deleted
|
||||
# by the host user on macOS.
|
||||
cleanup() {
|
||||
docker rm -f "$UPGRADE_CONTAINER" >/dev/null 2>&1 || true
|
||||
docker rm -f "$COMPLETE_CONTAINER" >/dev/null 2>&1 || true
|
||||
if [ -n "$staging_dir" ] && [ -d "$staging_dir" ]; then
|
||||
docker run --rm -v "$staging_dir:/cleanup" alpine rm -rf /cleanup 2>/dev/null || true
|
||||
rm -rf "$staging_dir" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
on_interrupt() {
|
||||
echo ""
|
||||
warn "Interrupted. Cleaning up..."
|
||||
# If db-config was chowned to PG17, restore for PG 15 rollback
|
||||
if [ -n "$db_config_vol" ] && [ -n "$current_image" ]; then
|
||||
docker run --rm -v "${db_config_vol}:/vol" "$current_image" \
|
||||
chown -R postgres:postgres /vol/ 2>/dev/null || true
|
||||
fi
|
||||
die "Interrupted."
|
||||
}
|
||||
trap on_interrupt INT
|
||||
|
||||
confirm() {
|
||||
if [ "$AUTO_CONFIRM" = true ]; then return 0; fi
|
||||
if ! test -t 0; then
|
||||
die "This script must be run interactively, or use --yes to skip prompts."
|
||||
fi
|
||||
printf '%s (y/N) ' "$1"
|
||||
read -r reply
|
||||
case "$reply" in
|
||||
[Yy]*) return 0 ;;
|
||||
*) echo "Aborted."; exit 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_sql_on() {
|
||||
local container=$1; shift
|
||||
docker exec -i \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$container" \
|
||||
psql -h localhost -U supabase_admin -d postgres -v ON_ERROR_STOP=1 "$@"
|
||||
}
|
||||
|
||||
wait_for_healthy() {
|
||||
local container=$1 retries=30
|
||||
while [ $retries -gt 0 ]; do
|
||||
if docker exec "$container" pg_isready -U postgres -h localhost >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
retries=$((retries - 1))
|
||||
sleep 1
|
||||
done
|
||||
die "Postgres in '$container' did not become ready in 30 seconds."
|
||||
}
|
||||
|
||||
# --- Pre-flight checks -----------------------------------------------------
|
||||
|
||||
preflight() {
|
||||
info "Running pre-flight checks"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "This script must be run as root (e.g. sudo bash $0)."
|
||||
fi
|
||||
|
||||
docker compose version >/dev/null 2>&1 || die "Docker Compose not found."
|
||||
command -v curl >/dev/null 2>&1 || die "curl is required (for downloading upgrade scripts)."
|
||||
[ -f docker-compose.yml ] || die "Run this script from the docker/ directory."
|
||||
[ -f docker-compose.pg17.yml ] || die "Missing docker-compose.pg17.yml."
|
||||
[ -f .env ] || die "Missing .env file."
|
||||
|
||||
# Resolve db-config volume (exact match on _db-config suffix or bare db-config)
|
||||
db_config_vol=$(docker volume ls --filter "name=db-config" --format '{{.Name}}' \
|
||||
| grep -E '^db-config$|_db-config$' | head -n 1)
|
||||
[ -n "$db_config_vol" ] || die "Could not find db-config volume. Is Supabase running?"
|
||||
|
||||
pg_password=$(grep '^POSTGRES_PASSWORD=' .env | cut -d '=' -f 2- | sed "s/^['\"]//;s/['\"]$//" | head -n 1)
|
||||
[ -n "$pg_password" ] || die "POSTGRES_PASSWORD not set in .env."
|
||||
|
||||
docker inspect "$DB_CONTAINER" >/dev/null 2>&1 \
|
||||
|| die "Container '$DB_CONTAINER' not found. Is Supabase running?"
|
||||
|
||||
current_image=$(docker inspect "$DB_CONTAINER" --format '{{.Config.Image}}')
|
||||
case "$current_image" in
|
||||
supabase/postgres:15.*|supabase.postgres:15.*) ;;
|
||||
supabase/postgres:17.*|supabase.postgres:17.*) die "Already running Postgres 17 ($current_image)." ;;
|
||||
*) die "Unexpected database image: $current_image" ;;
|
||||
esac
|
||||
|
||||
local status
|
||||
status=$(docker inspect "$DB_CONTAINER" --format '{{.State.Status}}')
|
||||
[ "$status" = "running" ] || die "'$DB_CONTAINER' is not running (status: $status)."
|
||||
[ -d "$DATA_DIR" ] || die "Data directory not found: $DATA_DIR"
|
||||
|
||||
if [ -d "$BACKUP_DIR" ]; then
|
||||
warn "Backup directory already exists: $BACKUP_DIR"
|
||||
warn "This is likely from a previous upgrade attempt."
|
||||
warn "If you haven't verified that previous upgrade, roll back first:"
|
||||
warn " 1. docker compose -f docker-compose.yml -f docker-compose.pg17.yml down"
|
||||
warn " 2. rm -rf $DATA_DIR"
|
||||
warn " 3. mv $BACKUP_DIR $DATA_DIR"
|
||||
warn " 4. docker compose -f docker-compose.yml -f docker-compose.pg15.yml run --rm db chown -R postgres:postgres /etc/postgresql-custom/"
|
||||
warn " 5. docker compose -f docker-compose.yml -f docker-compose.pg15.yml up -d"
|
||||
echo ""
|
||||
warn "Continuing will DELETE the existing backup permanently."
|
||||
confirm "Delete $BACKUP_DIR and start a fresh upgrade?"
|
||||
rm -rf "$BACKUP_DIR"
|
||||
fi
|
||||
if [ -d "$MIGRATION_DIR" ]; then
|
||||
rm -rf "$MIGRATION_DIR"
|
||||
fi
|
||||
|
||||
# Disk space
|
||||
local data_size_kb data_size_mb avail_kb avail_mb needed_mb
|
||||
data_size_kb=$(du -sk "$DATA_DIR" 2>/dev/null | cut -f1)
|
||||
[ -n "$data_size_kb" ] || die "Could not calculate data size for $DATA_DIR"
|
||||
data_size_mb=$((data_size_kb / 1024))
|
||||
avail_kb=$(df -k "$(dirname "$DATA_DIR")" | awk 'NR==2 { print $4 }')
|
||||
[ -n "$avail_kb" ] || die "Could not calculate available disk space for $(dirname "$DATA_DIR")"
|
||||
avail_mb=$((avail_kb / 1024))
|
||||
needed_mb=$((data_size_mb * 2 + 5000))
|
||||
echo " Data size: ${data_size_mb} MB"
|
||||
echo " Available space: ${avail_mb} MB"
|
||||
echo " Estimated need: ${needed_mb} MB"
|
||||
if [ "$avail_mb" -lt "$needed_mb" ]; then
|
||||
warn "Disk space may be insufficient."
|
||||
warn "pg_upgrade copies data; need ~2x data size + ~5 GB for the upgrade tarball."
|
||||
confirm "Continue anyway?"
|
||||
fi
|
||||
|
||||
# Incompatible extensions
|
||||
info "Checking for incompatible extensions"
|
||||
local incompatible
|
||||
incompatible=$(run_sql_on "$DB_CONTAINER" -A -t -c "
|
||||
SELECT string_agg(extname, ', ')
|
||||
FROM pg_extension
|
||||
WHERE extname IN ('timescaledb', 'plv8', 'plcoffee', 'plls');
|
||||
" 2>/dev/null | tr -d '[:space:]') || true
|
||||
|
||||
if [ -n "$incompatible" ]; then
|
||||
warn "Incompatible extensions found: $incompatible"
|
||||
warn "These do not exist in Postgres 17 and must be dropped before upgrading."
|
||||
warn "If you proceed, they will be dropped automatically."
|
||||
warn "The original data is preserved as a backup so you can roll back."
|
||||
confirm "Drop these extensions and continue with the upgrade?"
|
||||
drop_extensions="$incompatible"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " 1. Pull the Postgres 17 image"
|
||||
echo " 2. Build an upgrade tarball from the image (~1.2 GB compressed, temporary)"
|
||||
echo " 3. Stop all Supabase services"
|
||||
echo " 4. Run pg_upgrade (Postgres 15 -> 17)"
|
||||
echo " 5. Apply post-upgrade patches"
|
||||
echo " 6. Start Supabase with Postgres 17"
|
||||
echo " 7. Apply additional migrations"
|
||||
echo ""
|
||||
echo " Current image: $current_image"
|
||||
echo " Target image: $PG17_TARGET_IMAGE"
|
||||
echo " Upgrade image: $PG17_UPGRADE_IMAGE"
|
||||
echo " Data directory: $DATA_DIR"
|
||||
echo " Backup location: $BACKUP_DIR"
|
||||
echo ""
|
||||
confirm "Proceed with the upgrade?"
|
||||
}
|
||||
|
||||
# --- Step 1: Pull Postgres 17 image ----------------------------------------
|
||||
|
||||
pull_image() {
|
||||
info "Pulling Postgres 17 images"
|
||||
docker pull "$PG17_UPGRADE_IMAGE"
|
||||
if [ "$PG17_TARGET_IMAGE" != "$PG17_UPGRADE_IMAGE" ]; then
|
||||
docker pull "$PG17_TARGET_IMAGE"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Step 2: Build upgrade tarball -----------------------------------------
|
||||
#
|
||||
# Extracts PG 17 binaries, libraries, share data, and upgrade scripts from
|
||||
# the PG 17 Docker image into a tarball that initiate.sh can consume.
|
||||
#
|
||||
# The tarball uses the "non-nix" layout (17/bin, 17/lib, 17/share - no
|
||||
# nix_flake_version file), so initiate.sh sets LD_LIBRARY_PATH to find
|
||||
# the bundled libraries.
|
||||
|
||||
build_tarball() {
|
||||
local tmpbase="${TMPDIR:-/tmp}"
|
||||
staging_dir=$(mktemp -d "${tmpbase%/}/supabase-pg17-upgrade.XXXXXX")
|
||||
# World-writable so Docker containers can write to bind mounts on macOS,
|
||||
# where the VM's root user has no special access to host directories.
|
||||
chmod 777 "$staging_dir"
|
||||
echo " Staging directory: $staging_dir"
|
||||
|
||||
# Download upgrade scripts from the supabase/postgres repo (pinned to PG17_SCRIPTS_REF).
|
||||
# These are no longer bundled in the latest PG 17 Docker images.
|
||||
info "Downloading upgrade scripts (ref: $PG17_SCRIPTS_REF)"
|
||||
local scripts_base="https://raw.githubusercontent.com/supabase/postgres/${PG17_SCRIPTS_REF}/ansible/files/admin_api_scripts/pg_upgrade_scripts"
|
||||
mkdir -p "$staging_dir/scripts"
|
||||
for script in initiate.sh complete.sh common.sh pgsodium_getkey.sh check.sh prepare.sh; do
|
||||
curl -fsSL "$scripts_base/$script" -o "$staging_dir/scripts/$script" \
|
||||
|| die "Failed to download $script from GitHub"
|
||||
done
|
||||
|
||||
if [ -f "$TARBALL_CACHE" ]; then
|
||||
info "Using cached upgrade tarball: $TARBALL_CACHE"
|
||||
cp "$TARBALL_CACHE" "$staging_dir/pg_upgrade_bin.tar.gz"
|
||||
return
|
||||
fi
|
||||
|
||||
info "Building upgrade tarball from Postgres 17 image (first run)"
|
||||
docker run --rm --user root --entrypoint bash \
|
||||
-v "$staging_dir:/export" \
|
||||
"$PG17_UPGRADE_IMAGE" \
|
||||
-c '
|
||||
set -euo pipefail
|
||||
mkdir -p /export/17/bin /export/17/lib /export/17/share
|
||||
|
||||
echo " Copying binaries..."
|
||||
# Binaries in the nix profile are either ELF binaries or shell
|
||||
# wrappers that exec a .xxx-wrapped ELF from the nix store.
|
||||
# Extract the actual ELF binaries so they work outside nix.
|
||||
BIN_DIR=$(dirname $(readlink -f /usr/lib/postgresql/bin/postgres))
|
||||
for f in "$BIN_DIR"/*; do
|
||||
name=$(basename "$f")
|
||||
|
||||
# Skip nix wrapper-internal files
|
||||
case "$name" in .*-wrapped) continue ;; esac
|
||||
|
||||
# Check for ELF
|
||||
if [ -x "$f" ] && file -b "$f" | grep -q "ELF .* executable"; then
|
||||
cp "$f" /export/17/bin/"$name"
|
||||
else
|
||||
# Shell wrapper - extract the real .xxx-wrapped ELF path
|
||||
wrapped=$(grep -o "/nix/store/[^ \"]*-wrapped" "$f" 2>/dev/null | head -n 1 || true)
|
||||
if [ -n "$wrapped" ] && [ -f "$wrapped" ]; then
|
||||
cp "$wrapped" /export/17/bin/"$name"
|
||||
else
|
||||
cp "$f" /export/17/bin/"$name"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo " Copying libraries..."
|
||||
PKGLIBDIR=$(pg_config --pkglibdir)
|
||||
LIBDIR=$(pg_config --libdir)
|
||||
|
||||
# These paths may overlap (PKGLIBDIR and LIBDIR often point to the
|
||||
# same nix store path). Use cp -Lf to handle overwrites from
|
||||
# read-only nix store source files.
|
||||
cp -Lf "$PKGLIBDIR"/*.so /export/17/lib/ || echo " Warning: cp from $PKGLIBDIR failed" >&2
|
||||
cp -Lf "$LIBDIR"/*.so* /export/17/lib/ || echo " Warning: cp from $LIBDIR failed" >&2
|
||||
cp -Lf /nix/var/nix/profiles/default/lib/*.so* /export/17/lib/ || echo " Warning: cp from nix profile lib failed" >&2
|
||||
|
||||
echo " Copying share data..."
|
||||
|
||||
# Nix-built binaries resolve share dir relative to their location:
|
||||
# <bindir>/../share/postgresql/
|
||||
# so we need share/postgresql/ not just share/
|
||||
mkdir -p /export/17/share/postgresql
|
||||
|
||||
# Remove cyclic symlink (timezonesets/timezonesets -> timezonesets).
|
||||
rm -f /usr/share/postgresql/timezonesets/timezonesets 2>/dev/null || true
|
||||
|
||||
# Pre-create subdirectories so cp -rL does not need to mkdir them.
|
||||
# On macOS with Docker Desktop bind mount rejects mkdir with the nix store
|
||||
# read-only (dr-xr-xr-x) permissions; pre-creating with default
|
||||
# writable permissions fixed this
|
||||
mkdir -p /export/17/share/postgresql/{extension,timezonesets,tsearch_data}
|
||||
mkdir -p /export/17/share/postgresql/extension/{functions,procedures,tables,types}
|
||||
|
||||
cp -rL /usr/share/postgresql/* /export/17/share/postgresql/ || echo " Warning: cp share data had errors" >&2
|
||||
|
||||
# initiate.sh copies .control/.sql from PGLIBNEW to PGSHARENEW/extension/
|
||||
echo " Copying extension definitions to lib..."
|
||||
SHAREDIR=$(pg_config --sharedir)
|
||||
cp "$SHAREDIR"/extension/*.control /export/17/lib/ || echo " Warning: cp .control from $SHAREDIR/extension failed" >&2
|
||||
cp "$SHAREDIR"/extension/*.sql /export/17/lib/ || echo " Warning: cp .sql from $SHAREDIR/extension failed" >&2
|
||||
|
||||
# Verify critical files before creating tarball
|
||||
echo " Checking for key files..."
|
||||
[ -f /export/17/bin/postgres ] || { echo "Error: bin/postgres missing"; exit 1; }
|
||||
[ -f /export/17/share/postgresql/timezonesets/Default ] || { echo "Error: timezonesets/Default missing"; exit 1; }
|
||||
ls /export/17/share/postgresql/extension/*.control >/dev/null 2>&1 || { echo "Error: no .control files in extension/"; exit 1; }
|
||||
ls /export/17/lib/*.so >/dev/null 2>&1 || { echo "Error: no .so files in lib/"; exit 1; }
|
||||
|
||||
echo " Creating tarball (this may take several minutes)..."
|
||||
cd /export && tar czf pg_upgrade_bin.tar.gz 17/
|
||||
|
||||
echo " Tarball: $(du -sh /export/pg_upgrade_bin.tar.gz | cut -f1)"
|
||||
'
|
||||
|
||||
# Cache for next run
|
||||
cp "$staging_dir/pg_upgrade_bin.tar.gz" "$TARBALL_CACHE"
|
||||
info "Tarball cached at $TARBALL_CACHE"
|
||||
}
|
||||
|
||||
# --- Step 3: Drop incompatible extensions ----------------------------------
|
||||
|
||||
drop_incompatible_extensions() {
|
||||
if [ -z "$drop_extensions" ]; then
|
||||
return
|
||||
fi
|
||||
info "Dropping incompatible extensions"
|
||||
|
||||
local ext
|
||||
echo "$drop_extensions" | tr ',' '\n' | while read -r ext; do
|
||||
ext=$(echo "$ext" | tr -d '[:space:]')
|
||||
[ -z "$ext" ] && continue
|
||||
echo " DROP EXTENSION $ext CASCADE"
|
||||
run_sql_on "$DB_CONTAINER" -c "DROP EXTENSION IF EXISTS \"$ext\" CASCADE;"
|
||||
done
|
||||
}
|
||||
|
||||
# --- Step 4: Stop services and back up -------------------------------------
|
||||
|
||||
stop_and_backup() {
|
||||
info "Backing up pgsodium root key"
|
||||
local key_backup="./volumes/db/pgsodium_root.key.bak.pg15"
|
||||
docker run --rm -v "${db_config_vol}:/src:ro" -v "$(pwd)/volumes/db:/dst" \
|
||||
alpine cp /src/pgsodium_root.key /dst/pgsodium_root.key.bak.pg15 \
|
||||
|| die "Failed to back up pgsodium root key from db-config volume."
|
||||
echo " Saved to: $key_backup"
|
||||
|
||||
info "Stopping all Supabase services"
|
||||
docker compose down
|
||||
|
||||
echo " Original data will be preserved as: $BACKUP_DIR"
|
||||
}
|
||||
|
||||
# --- Step 5: Run pg_upgrade via initiate.sh + complete.sh ------------------
|
||||
#
|
||||
# Host directories are mounted at non-standard paths (/mnt/host-*) with
|
||||
# symlinks at the paths the upgrade scripts expect. This lets complete.sh's
|
||||
# CI wrapper (which does rm/mv/ln on /var/lib/postgresql/data and
|
||||
# /data_migration) operate on symlinks rather than bind mounts.
|
||||
|
||||
run_upgrade() {
|
||||
local abs_data_dir abs_migration_dir
|
||||
|
||||
mkdir -p "$MIGRATION_DIR"
|
||||
# World-writable for macOS Docker bind mount compatibility (see build_tarball)
|
||||
chmod 777 "$MIGRATION_DIR"
|
||||
abs_data_dir=$(cd "$DATA_DIR" && pwd)
|
||||
abs_migration_dir=$(cd "$MIGRATION_DIR" && pwd)
|
||||
|
||||
info "Starting upgrade container"
|
||||
docker run -d --name "$UPGRADE_CONTAINER" \
|
||||
--entrypoint sleep \
|
||||
-v "${abs_data_dir}:/mnt/host-pgdata" \
|
||||
-v "${abs_migration_dir}:/mnt/host-migration" \
|
||||
-v "${db_config_vol}:/etc/postgresql-custom" \
|
||||
-v "${staging_dir}:/tmp/staging:ro" \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$current_image" \
|
||||
infinity
|
||||
|
||||
info "Preparing upgrade environment"
|
||||
docker exec "$UPGRADE_CONTAINER" bash -c '
|
||||
# Symlink bind mounts to the paths the upgrade scripts expect
|
||||
rm -rf /var/lib/postgresql/data
|
||||
ln -s /mnt/host-pgdata /var/lib/postgresql/data
|
||||
ln -s /mnt/host-migration /data_migration
|
||||
|
||||
mkdir -p /tmp/persistent /tmp/upgrade /tmp/pg_upgrade
|
||||
cp /tmp/staging/pg_upgrade_bin.tar.gz /tmp/persistent/
|
||||
cp /tmp/staging/scripts/*.sh /tmp/upgrade/
|
||||
chmod +x /tmp/upgrade/*.sh
|
||||
|
||||
# Patch CI_start_postgres to use "restart" instead of "start" so it
|
||||
# is idempotent (initiate.sh starts postgres for top-level queries,
|
||||
# then handle_extensions calls CI_start_postgres again)
|
||||
sed -i "s/pg_ctl start -o/pg_ctl restart -o/g" /tmp/upgrade/common.sh
|
||||
|
||||
# Patch PGSHARENEW to match nix binary expectations (share/postgresql/)
|
||||
sed -i "s|PGSHARENEW=\"\$PG_UPGRADE_BIN_DIR/share\"|PGSHARENEW=\"\$PG_UPGRADE_BIN_DIR/share/postgresql\"|" /tmp/upgrade/initiate.sh
|
||||
'
|
||||
|
||||
info "Starting Postgres 15 in upgrade container"
|
||||
docker exec "$UPGRADE_CONTAINER" bash -c '
|
||||
su postgres -c "pg_ctl start -o \"-c config_file=/etc/postgresql/postgresql.conf\" -l /tmp/postgres.log"
|
||||
'
|
||||
wait_for_healthy "$UPGRADE_CONTAINER"
|
||||
|
||||
# initiate.sh expects the PG 17 binaries tarball at /tmp/persistent/pg_upgrade_bin.tar.gz
|
||||
# (hardcoded path - copied there during container setup above).
|
||||
#
|
||||
# Env vars for the unwrapped nix ELF binaries in the tarball:
|
||||
# LD_LIBRARY_PATH - find libpq, libssl, etc. (RUNPATH points to absent nix store paths)
|
||||
# NIX_PGLIBDIR - postgres uses this to find extension .so files
|
||||
|
||||
info "Running initiate.sh (pg_upgrade: Postgres 15 -> 17)"
|
||||
echo " This may take several minutes depending on database size..."
|
||||
echo ""
|
||||
if ! docker exec \
|
||||
-e IS_CI=true \
|
||||
-e PG_MAJOR_VERSION=17 \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
-e LD_LIBRARY_PATH=/tmp/pg_upgrade_bin/17/lib \
|
||||
-e NIX_PGLIBDIR=/tmp/pg_upgrade_bin/17/lib \
|
||||
"$UPGRADE_CONTAINER" \
|
||||
/tmp/upgrade/initiate.sh 17; then
|
||||
echo ""
|
||||
warn "initiate.sh failed. Its cleanup may have restored the original state"
|
||||
warn "(re-enabled extensions, revoked superuser). Your data directory is"
|
||||
warn "unchanged - no data was moved or deleted."
|
||||
warn ""
|
||||
warn "Check the output above for the root cause, fix it, and re-run."
|
||||
docker rm -f "$UPGRADE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "initiate.sh failed"
|
||||
fi
|
||||
|
||||
info "initiate.sh completed successfully"
|
||||
docker rm -f "$UPGRADE_CONTAINER" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# --- Step 6: Run complete.sh in a native PG 17 container -------------------
|
||||
#
|
||||
# complete.sh applies post-upgrade patches (pg_net grants, vault re-encryption,
|
||||
# pg_cron, predefined roles, vacuumdb, etc.). We run it in a PG17 container
|
||||
# where the binaries are native - no nix extraction or LD_LIBRARY_PATH needed.
|
||||
|
||||
run_complete() {
|
||||
local abs_migration_dir
|
||||
|
||||
abs_migration_dir=$(cd "$MIGRATION_DIR" && pwd)
|
||||
|
||||
info "Starting PG17 container for complete.sh"
|
||||
docker run -d --name "$COMPLETE_CONTAINER" \
|
||||
--entrypoint sleep \
|
||||
-v "${abs_migration_dir}:/mnt/host-migration" \
|
||||
-v "${db_config_vol}:/etc/postgresql-custom" \
|
||||
-v "${staging_dir}:/tmp/staging:ro" \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$PG17_UPGRADE_IMAGE" \
|
||||
infinity
|
||||
|
||||
info "Preparing complete.sh environment"
|
||||
# Save original db-config ownership so we can restore it if complete.sh fails.
|
||||
# complete.sh needs PG 17 ownership to start postgres, but if it fails the
|
||||
# user needs to fall back to PG 15 which uses a different uid.
|
||||
docker exec "$COMPLETE_CONTAINER" bash -c '
|
||||
stat -c "%u:%g" /etc/postgresql-custom/pgsodium_root.key 2>/dev/null > /tmp/dbconfig_owner || true
|
||||
'
|
||||
|
||||
docker exec "$COMPLETE_CONTAINER" bash -c '
|
||||
# Symlink bind mount so complete.sh CI wrapper can mv/rm/ln
|
||||
ln -s /mnt/host-migration /data_migration
|
||||
|
||||
# Remove the image default data dir (complete.sh creates a symlink here)
|
||||
rm -rf /var/lib/postgresql/data
|
||||
|
||||
# Fix ownership on db-config volume (PG15 uid differs from PG17)
|
||||
chown -R postgres:postgres /etc/postgresql-custom/
|
||||
|
||||
# PG17 config includes this directory; may not exist from PG15
|
||||
mkdir -p /etc/postgresql-custom/conf.d
|
||||
|
||||
mkdir -p /tmp/upgrade
|
||||
|
||||
# Copy upgrade scripts
|
||||
cp /tmp/staging/scripts/*.sh /tmp/upgrade/
|
||||
chmod +x /tmp/upgrade/*.sh
|
||||
|
||||
# Patch --new-bin to use native bindir (we are in a PG17 container,
|
||||
# no need for /tmp/pg_upgrade_bin/ paths)
|
||||
sed -i "s|BINDIR=\"/tmp/pg_upgrade_bin/\$PG_MAJOR_VERSION/bin\"|BINDIR=\$(pg_config --bindir)|g" /tmp/upgrade/common.sh
|
||||
'
|
||||
|
||||
info "Running complete.sh (post-upgrade patches, vacuum analyze)"
|
||||
docker exec \
|
||||
-e IS_CI=true \
|
||||
-e PG_MAJOR_VERSION=17 \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$COMPLETE_CONTAINER" \
|
||||
/tmp/upgrade/complete.sh || true
|
||||
|
||||
# complete.sh's ERR trap exits with 0 in some cases; check status file
|
||||
local status
|
||||
status=$(docker exec "$COMPLETE_CONTAINER" cat /tmp/pg-upgrade-status 2>/dev/null || echo "unknown")
|
||||
if [ "$status" != "complete" ]; then
|
||||
warn "complete.sh failed. Postgres log:"
|
||||
docker exec "$COMPLETE_CONTAINER" cat /tmp/postgres.log 2>/dev/null || true
|
||||
echo ""
|
||||
# Restore db-config ownership so PG 15 can start for rollback
|
||||
warn "Restoring db-config ownership for PG15..."
|
||||
local orig_owner
|
||||
orig_owner=$(docker exec "$COMPLETE_CONTAINER" cat /tmp/dbconfig_owner 2>/dev/null || true)
|
||||
if [ -n "$orig_owner" ]; then
|
||||
docker exec "$COMPLETE_CONTAINER" chown -R "$orig_owner" /etc/postgresql-custom/ 2>/dev/null || true
|
||||
fi
|
||||
docker rm -f "$COMPLETE_CONTAINER" >/dev/null 2>&1 || true
|
||||
echo ""
|
||||
echo " Your Postgres 15 data is unchanged (data swap has not happened yet)."
|
||||
echo " To restart Postgres 15:"
|
||||
echo " rm -rf $MIGRATION_DIR"
|
||||
echo " docker compose -f docker-compose.yml -f docker-compose.pg15.yml up -d"
|
||||
echo ""
|
||||
die "complete.sh failed (status: $status)"
|
||||
fi
|
||||
|
||||
info "complete.sh finished successfully"
|
||||
docker rm -f "$COMPLETE_CONTAINER" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# --- Step 7: Swap data directories -----------------------------------------
|
||||
|
||||
swap_data() {
|
||||
info "Swapping data directories"
|
||||
|
||||
echo " $DATA_DIR -> $BACKUP_DIR"
|
||||
mv "$DATA_DIR" "$BACKUP_DIR"
|
||||
|
||||
echo " $MIGRATION_DIR/pgdata -> $DATA_DIR"
|
||||
mv "$MIGRATION_DIR/pgdata" "$DATA_DIR"
|
||||
rm -rf "$MIGRATION_DIR"
|
||||
}
|
||||
|
||||
# --- Step 8: Start Postgres 17 ---------------------------------------------
|
||||
|
||||
start_pg17() {
|
||||
info "Starting Supabase with Postgres 17"
|
||||
|
||||
# Ensure db-config volume has correct ownership and structure for PG 17.
|
||||
# complete.sh does this too, but just in case of partial
|
||||
# failures from previous runs.
|
||||
docker run --rm -v "${db_config_vol}:/vol" "$PG17_TARGET_IMAGE" sh -c '
|
||||
mkdir -p /vol/conf.d
|
||||
chown -R postgres:postgres /vol/
|
||||
'
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.pg17.yml up -d
|
||||
|
||||
echo " Waiting for Postgres 17 to be ready..."
|
||||
local retries=60
|
||||
while [ $retries -gt 0 ]; do
|
||||
if docker exec "$DB_CONTAINER" pg_isready -U postgres -h localhost >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
retries=$((retries - 1))
|
||||
sleep 2
|
||||
done
|
||||
[ $retries -gt 0 ] || die "Postgres 17 did not start within 120 seconds."
|
||||
|
||||
local new_version
|
||||
new_version=$(run_sql_on "$DB_CONTAINER" -A -t -c "SHOW server_version;" 2>/dev/null | head -n 1)
|
||||
echo " Postgres version: $new_version"
|
||||
case "$new_version" in
|
||||
17.*) ;;
|
||||
*) die "Expected Postgres 17.x, got: $new_version" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- Step 9: Apply migrations not covered by complete.sh -------------------
|
||||
#
|
||||
# These PG 17 migrations run on fresh installs via initdb but not after
|
||||
# pg_upgrade (init scripts don't rerun when PG_VERSION already exists).
|
||||
# complete.sh doesn't cover them either. On the platform a tracked migration
|
||||
# runner (dbmate) applies them; self-hosted has no such tracking, so we apply
|
||||
# the idempotent, safe-on-an-existing-DB ones directly here.
|
||||
#
|
||||
# Source: postgres/migrations/db/migrations/
|
||||
# - 20250710151649_supabase_read_only_user_default_transaction_read_only.sql
|
||||
# - 20251001204436_predefined_role_grants.sql (supabase_etl_admin + pg_monitor)
|
||||
# - 20251105172723_grant_pg_reload_conf_to_postgres.sql
|
||||
# - 20251121132723_correct_search_path_pgbouncer.sql
|
||||
# - 20260211120934_supabase_privileged_role.sql (target image's supautils.conf
|
||||
# references this role; it must exist or supautils config is broken)
|
||||
# - 20260413000000_fix-authenticator-session-preload-libraries.sql
|
||||
# - 20260421000001_rescope_pg_graphql_access_trigger.sql
|
||||
#
|
||||
# NOT applied:
|
||||
# - 20260421000000_pg_graphql-off-by-default.sql: drops pg_graphql. Safe on a
|
||||
# fresh install, but destructive on an existing DB that uses pg_graphql.
|
||||
#
|
||||
# This step also reconciles extension versions (complete.sh ran with the .063
|
||||
# binaries) and the pg_cron 1.6 -> 1.6.4 version label. See below.
|
||||
|
||||
apply_role_migrations() {
|
||||
info "Applying Postgres 17 migrations"
|
||||
|
||||
# Fix collation version mismatch first (upgrade used glibc 2.39, target
|
||||
# image may use glibc 2.40). Do this before any other SQL to suppress
|
||||
# the noisy warnings on every subsequent command.
|
||||
for db in postgres template1 _supabase; do
|
||||
docker exec -i -e PGPASSWORD="$pg_password" "$DB_CONTAINER" \
|
||||
psql -h localhost -U supabase_admin -d "$db" \
|
||||
-c "ALTER DATABASE \"$db\" REFRESH COLLATION VERSION;" || true
|
||||
done
|
||||
|
||||
# Create supabase_etl_admin role (doesn't exist in PG 15 images).
|
||||
# Must be created before running predefined_role_grants.sql which
|
||||
# assumes it exists.
|
||||
run_sql_on "$DB_CONTAINER" -c "
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'supabase_etl_admin') THEN
|
||||
CREATE USER supabase_etl_admin WITH LOGIN REPLICATION;
|
||||
GRANT pg_read_all_data TO supabase_etl_admin;
|
||||
GRANT CREATE ON DATABASE postgres TO supabase_etl_admin;
|
||||
END IF;
|
||||
END
|
||||
\$\$;" || true
|
||||
|
||||
# Run the migration files directly from the PG 17 container image.
|
||||
# They're idempotent (IF EXISTS / IF NOT EXISTS guards).
|
||||
local migration_dir="/docker-entrypoint-initdb.d/migrations"
|
||||
local migrations="
|
||||
20250710151649_supabase_read_only_user_default_transaction_read_only.sql
|
||||
20251001204436_predefined_role_grants.sql
|
||||
20251105172723_grant_pg_reload_conf_to_postgres.sql
|
||||
20251121132723_correct_search_path_pgbouncer.sql
|
||||
20260211120934_supabase_privileged_role.sql
|
||||
20260413000000_fix-authenticator-session-preload-libraries.sql
|
||||
20260421000001_rescope_pg_graphql_access_trigger.sql
|
||||
"
|
||||
|
||||
for m in $migrations; do
|
||||
echo " Running: $m"
|
||||
docker exec -i \
|
||||
-e PGPASSWORD="$pg_password" \
|
||||
"$DB_CONTAINER" \
|
||||
psql -h localhost -U supabase_admin -d postgres -v ON_ERROR_STOP=1 \
|
||||
-f "${migration_dir}/${m}" || warn " $m failed (non-fatal)"
|
||||
done
|
||||
|
||||
# Reconcile extension versions to the target image. pg_upgrade generates
|
||||
# update_extensions.sql, but complete.sh runs it inside the .063 container, so
|
||||
# extensions only get updated to .063's versions. The final image ships newer
|
||||
# .so files, leaving the catalog version behind what's loaded.
|
||||
#
|
||||
# Mirror what update_extensions.sql does, but against the running target image
|
||||
# and for every installed extension: ALTER EXTENSION ... UPDATE brings each one
|
||||
# up to the image's default version. This is generic on purpose - it tracks the
|
||||
# image (matching platform behavior) and stays correct across future image
|
||||
# bumps without maintaining a hardcoded list. ALTER ... UPDATE is a no-op when
|
||||
# already at the default. Per-extension exceptions are caught so one extension
|
||||
# with no update path (e.g. pg_cron 1.6 -> 1.6.4, handled below) does not abort
|
||||
# the rest.
|
||||
info "Reconciling extension versions to the target image"
|
||||
run_sql_on "$DB_CONTAINER" -c "
|
||||
DO \$\$
|
||||
DECLARE r record;
|
||||
BEGIN
|
||||
FOR r IN SELECT extname FROM pg_extension LOOP
|
||||
BEGIN
|
||||
EXECUTE format('ALTER EXTENSION %I UPDATE', r.extname);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'skipped extension %: %', r.extname, SQLERRM;
|
||||
END;
|
||||
END LOOP;
|
||||
END
|
||||
\$\$;" || warn "extension version reconcile had errors (non-fatal)"
|
||||
|
||||
# pg_cron: PG 15 registers the extension as '1.6'; PG 17 images package it as
|
||||
# '1.6.4' with no '1.6' -> '1.6.4' update path, so ALTER EXTENSION ... UPDATE
|
||||
# has no path to follow. complete.sh only fixes this when pg_cron is owned by
|
||||
# 'postgres' (it drops + recreates). Manually-enabled instances are often
|
||||
# owned by supabase_admin, where that path is skipped. Reconcile the catalog
|
||||
# label directly: the loaded .so and the SQL objects are already
|
||||
# 1.6.4-equivalent (no SQL delta between 1.6 and 1.6.4). No DROP, so any
|
||||
# scheduled jobs are untouched.
|
||||
run_sql_on "$DB_CONTAINER" -c "
|
||||
DO \$\$
|
||||
DECLARE want text;
|
||||
BEGIN
|
||||
SELECT default_version INTO want FROM pg_available_extensions WHERE name = 'pg_cron';
|
||||
IF want IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron' AND extversion <> want) THEN
|
||||
UPDATE pg_extension SET extversion = want WHERE extname = 'pg_cron';
|
||||
END IF;
|
||||
END
|
||||
\$\$;" || warn "pg_cron version reconcile had errors (non-fatal)"
|
||||
}
|
||||
|
||||
# --- Step 10: Verify ------------------------------------------------------
|
||||
|
||||
verify() {
|
||||
info "Verification"
|
||||
|
||||
local version
|
||||
version=$(run_sql_on "$DB_CONTAINER" -A -t -c "SELECT version();" 2>/dev/null | head -n 1)
|
||||
echo " $version"
|
||||
|
||||
echo ""
|
||||
echo " Extensions:"
|
||||
run_sql_on "$DB_CONTAINER" -c \
|
||||
"SELECT extname, extversion FROM pg_extension ORDER BY extname;"
|
||||
|
||||
echo ""
|
||||
info "Upgrade complete!"
|
||||
echo ""
|
||||
echo " To use Postgres 17 going forward, always include the override:"
|
||||
echo " docker compose -f docker-compose.yml -f docker-compose.pg17.yml up -d"
|
||||
echo ""
|
||||
echo " Postgres 15 backup: $BACKUP_DIR"
|
||||
echo " pgsodium key backup: ./volumes/db/pgsodium_root.key.bak.pg15"
|
||||
echo " Once satisfied, you can reclaim space:"
|
||||
echo " rm -rf $BACKUP_DIR ./volumes/db/pg17_upgrade_bin_*.tar.gz"
|
||||
echo ""
|
||||
echo " Rollback (if needed):"
|
||||
echo " 1. docker compose -f docker-compose.yml -f docker-compose.pg17.yml down"
|
||||
echo " 2. rm -rf $DATA_DIR"
|
||||
echo " 3. mv $BACKUP_DIR $DATA_DIR"
|
||||
echo " 4. docker compose -f docker-compose.yml -f docker-compose.pg15.yml run --rm db chown -R postgres:postgres /etc/postgresql-custom/"
|
||||
echo " 5. docker compose -f docker-compose.yml -f docker-compose.pg15.yml up -d"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# --- Main -------------------------------------------------------------------
|
||||
|
||||
main() {
|
||||
echo ""
|
||||
echo "Supabase Self-Hosted: Postgres 15 -> 17 Upgrade"
|
||||
echo "================================================"
|
||||
|
||||
preflight
|
||||
pull_image
|
||||
build_tarball
|
||||
drop_incompatible_extensions
|
||||
stop_and_backup
|
||||
run_upgrade
|
||||
run_complete
|
||||
swap_data
|
||||
start_pg17
|
||||
apply_role_migrations
|
||||
verify
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,145 @@
|
||||
# Docker image version updates in docker-compose.yml
|
||||
|
||||
## 2026-08-03
|
||||
- supabase/studio:2026.08.03-sha-022b374 (prev supabase/studio:2026.07.07-sha-a6a04f2)
|
||||
- kong/kong:3.9.3 (prev kong/kong:3.9.1)
|
||||
|
||||
## 2026-07-07
|
||||
- supabase/studio:2026.07.07-sha-a6a04f2 (prev supabase/studio:2026.06.03-sha-0bca601)
|
||||
|
||||
## 2026-06-17
|
||||
- supabase/postgres:17.6.1.136 (prev supabase/postgres:15.8.1.085)
|
||||
|
||||
## 2026-06-03
|
||||
- supabase/studio:2026.06.03-sha-0bca601 (prev supabase/studio:2026.04.27-sha-5f60601)
|
||||
- supabase/gotrue:v2.189.0 (prev supabase/gotrue:v2.186.0)
|
||||
- postgrest/postgrest:v14.12 (prev postgrest/postgrest:v14.8)
|
||||
- supabase/realtime:v2.102.3 (prev supabase/realtime:v2.76.5)
|
||||
- supabase/storage-api:v1.60.4 (prev supabase/storage-api:v1.48.26)
|
||||
- supabase/postgres-meta:v0.96.6 (prev supabase/postgres-meta:v0.96.3)
|
||||
- supabase/edge-runtime:v1.74.0 (prev supabase/edge-runtime:v1.71.2)
|
||||
- supabase/supavisor:2.9.5 (prev supabase/supavisor:2.7.4)
|
||||
- supabase/logflare:1.43.1 (prev supabase/logflare:1.36.1)
|
||||
|
||||
## 2026-04-27
|
||||
- supabase/studio:2026.04.27-sha-5f60601 (prev supabase/studio:2026.04.08-sha-205cbe7)
|
||||
|
||||
## 2026-04-08
|
||||
- supabase/studio:2026.04.08-sha-205cbe7 (prev supabase/studio:2026.03.16-sha-5528817)
|
||||
- postgrest/postgrest:v14.8 (prev postgrest/postgrest:v14.6)
|
||||
- supabase/storage-api:v1.48.26 (prev supabase/storage-api:v1.44.2)
|
||||
- supabase/postgres-meta:v0.96.3 (prev supabase/postgres-meta:v0.95.2)
|
||||
- supabase/logflare:1.36.1 (prev supabase/logflare:1.31.2)
|
||||
|
||||
## 2026-03-16
|
||||
- supabase/studio:2026.03.16-sha-5528817 (prev supabase/studio:2026.02.16-sha-26c615c)
|
||||
- kong/kong:3.9.1 (prev kong:2.8.1)
|
||||
- postgrest/postgrest:v14.6 (prev postgrest/postgrest:v14.5)
|
||||
- supabase/storage-api:v1.44.2 (prev supabase/storage-api:v1.37.8)
|
||||
- supabase/edge-runtime:v1.71.2 (prev supabase/edge-runtime:v1.70.3)
|
||||
|
||||
## 2026-02-16
|
||||
- supabase/studio:2026.02.16-sha-26c615c (prev supabase/studio:2026.01.27-sha-6aa59ff)
|
||||
- supabase/gotrue:v2.186.0 (prev supabase/gotrue:v2.185.0)
|
||||
- postgrest/postgrest:v14.5 (prev postgrest/postgrest:v14.3)
|
||||
- supabase/realtime:v2.76.5 (prev supabase/realtime:v2.72.0)
|
||||
- supabase/storage-api:v1.37.8 (prev supabase/storage-api:v1.37.1)
|
||||
- supabase/edge-runtime:v1.70.3 (prev supabase/edge-runtime:v1.70.0)
|
||||
- supabase/logflare:1.31.2 (prev supabase/logflare:1.30.3)
|
||||
- timberio/vector:0.53.0-alpine (prev timberio/vector:0.28.1-alpine)
|
||||
|
||||
## 2026-02-05
|
||||
- supabase/storage-api:v1.37.1 (prev supabase/storage-api:v1.33.5)
|
||||
|
||||
## 2026-01-27
|
||||
- supabase/studio:2026.01.27-sha-6aa59ff (prev supabase/studio:2025.12.17-sha-43f4f7f)
|
||||
- supabase/gotrue:v2.185.0 (prev supabase/gotrue:v2.184.0)
|
||||
- postgrest/postgrest:v14.3 (prev postgrest/postgrest:v14.1)
|
||||
- supabase/realtime:v2.72.0 (prev supabase/realtime:v2.68.0)
|
||||
- supabase/storage-api:v1.33.5 (prev supabase/storage-api:v1.33.0)
|
||||
- darthsim/imgproxy:v3.30.1 (prev darthsim/imgproxy:v3.8.0)
|
||||
- supabase/postgres-meta:v0.95.2 (prev supabase/postgres-meta:v0.95.1)
|
||||
- supabase/edge-runtime:v1.70.0 (prev supabase/edge-runtime:v1.69.28)
|
||||
- supabase/logflare:1.30.3 (prev supabase/logflare:1.27.0)
|
||||
|
||||
## 2025-12-18
|
||||
- supabase/studio:2025.12.17-sha-43f4f7f (prev supabase/studio:2025.12.09-sha-434634f)
|
||||
- supabase/gotrue:v2.184.0 (prev supabase/gotrue:v2.183.0)
|
||||
- supabase/postgres-meta:v0.95.1 (prev supabase/postgres-meta:v0.93.1)
|
||||
- supabase/logflare:1.27.0 (prev supabase/logflare:1.26.25)
|
||||
|
||||
## 2025-12-10
|
||||
- supabase/studio:2025.12.09-sha-434634f (prev supabase/studio:2025.11.26-sha-8f096b5)
|
||||
- postgrest/postgrest:v14.1 (prev postgrest/postgrest:v13.0.7)
|
||||
- supabase/realtime:v2.68.0 (prev supabase/realtime:v2.65.3)
|
||||
- supabase/storage-api:v1.33.0 (prev supabase/storage-api:v1.32.0)
|
||||
- supabase/edge-runtime:v1.69.28 (prev supabase/edge-runtime:v1.69.25)
|
||||
- supabase/logflare:1.26.25 (prev supabase/logflare:1.26.13)
|
||||
|
||||
## 2025-11-26
|
||||
- supabase/studio:2025.11.26-sha-8f096b5 (prev supabase/studio:2025.11.24-sha-d990ae8)
|
||||
- supabase/realtime:v2.65.3 (prev supabase/realtime:v2.65.2)
|
||||
- supabase/logflare:1.26.13 (prev supabase/logflare:1.26.12)
|
||||
|
||||
## 2025-11-25
|
||||
- supabase/studio:2025.11.24-sha-d990ae8 (prev supabase/studio:2025.11.10-sha-5291fe3)
|
||||
- supabase/gotrue:v2.183.0 (prev supabase/gotrue:v2.182.1)
|
||||
- supabase/realtime:v2.65.2 (prev supabase/realtime:v2.63.0)
|
||||
- supabase/storage-api:v1.32.0 (prev supabase/storage-api:v1.29.0)
|
||||
- supabase/edge-runtime:v1.69.25 (prev supabase/edge-runtime:v1.69.23)
|
||||
- supabase/logflare:1.26.12 (prev supabase/logflare:1.22.6)
|
||||
|
||||
## 2025-11-12
|
||||
- supabase/studio:2025.11.10-sha-5291fe3 (prev supabase/studio:2025.10.27-sha-85b84e0)
|
||||
- supabase/gotrue:v2.182.1 (prev supabase/gotrue:v2.180.0)
|
||||
- supabase/realtime:v2.63.0 (prev supabase/realtime:v2.57.2)
|
||||
- supabase/storage-api:v1.29.0 (prev supabase/storage-api:v1.28.2)
|
||||
- supabase/edge-runtime:v1.69.23 (prev supabase/edge-runtime:v1.69.15)
|
||||
- supabase/supavisor:2.7.4 (prev supabase/supavisor:2.7.3)
|
||||
|
||||
## 2025-10-28
|
||||
- supabase/studio:2025.10.27-sha-85b84e0 (prev supabase/studio:2025.10.20-sha-5005fc6)
|
||||
- supabase/realtime:v2.57.2 (prev supabase/realtime:v2.56.0)
|
||||
- supabase/storage-api:v1.28.2 (prev supabase/storage-api:v1.28.1)
|
||||
- supabase/postgres-meta:v0.93.1 (prev supabase/postgres-meta:v0.93.0)
|
||||
- supabase/edge-runtime:v1.69.15 (prev supabase/edge-runtime:v1.69.14)
|
||||
|
||||
## 2025-10-21
|
||||
- supabase/studio:2025.10.20-sha-5005fc6 (prev supabase/studio:2025.10.01-sha-8460121)
|
||||
- supabase/realtime:v2.56.0 (prev supabase/realtime:v2.51.11)
|
||||
- supabase/storage-api:v1.28.1 (prev supabase/storage-api:v1.28.0)
|
||||
- supabase/postgres-meta:v0.93.0 (prev supabase/postgres-meta:v0.91.6)
|
||||
- supabase/edge-runtime:v1.69.14 (prev supabase/edge-runtime:v1.69.6)
|
||||
- supabase/supavisor:2.7.3 (prev supabase/supavisor:2.7.0)
|
||||
|
||||
## 2025-10-13
|
||||
- supabase/logflare:1.22.6 (prev supabase/logflare:1.22.4)
|
||||
|
||||
## 2025-10-08
|
||||
- supabase/studio:2025.10.01-sha-8460121 (prev supabase/studio:2025.06.30-sha-6f5982d)
|
||||
- supabase/gotrue:v2.180.0 (prev supabase/gotrue:v2.177.0)
|
||||
- postgrest/postgrest:v13.0.7 (prev postgrest/postgrest:v12.2.12)
|
||||
- supabase/realtime:v2.51.11 (prev supabase/realtime:v2.34.47)
|
||||
- supabase/storage-api:v1.28.0 (prev supabase/storage-api:v1.25.7)
|
||||
- supabase/postgres-meta:v0.91.6 (prev supabase/postgres-meta:v0.91.0)
|
||||
- supabase/logflare:1.22.4 (prev supabase/logflare:1.14.2)
|
||||
- supabase/postgres:15.8.1.085 (prev supabase/postgres:15.8.1.060)
|
||||
- supabase/supavisor:2.7.0 (prev supabase/supavisor:2.5.7)
|
||||
|
||||
## 2025-07-15
|
||||
- supabase/gotrue:v2.177.0 (prev supabase/gotrue:v2.176.1)
|
||||
- supabase/storage-api:v1.25.7 (prev supabase/storage-api:v1.24.7)
|
||||
- supabase/postgres-meta:v0.91.0 (prev supabase/postgres-meta:v0.89.3)
|
||||
- supabase/supavisor:2.5.7 (prev supabase/supavisor:2.5.6)
|
||||
|
||||
## 2025-07-02
|
||||
- supabase/studio:2025.06.30-sha-6f5982d (prev supabase/studio:2025.06.02-sha-8f2993d)
|
||||
- supabase/gotrue:v2.176.1 (prev supabase/gotrue:v2.174.0)
|
||||
- supabase/storage-api:v1.24.7 (prev supabase/storage-api:v1.23.0)
|
||||
- supabase/supavisor:2.5.6 (prev supabase/supavisor:2.5.1)
|
||||
|
||||
## 2025-06-03
|
||||
- supabase/studio:2025.06.02-sha-8f2993d (prev supabase/studio:2025.05.19-sha-3487831)
|
||||
- supabase/gotrue:v2.174.0 (prev supabase/gotrue:v2.172.1)
|
||||
- supabase/storage-api:v1.23.0 (prev supabase/storage-api:v1.22.17)
|
||||
- supabase/postgres-meta:v0.89.3 (prev supabase/postgres-meta:v0.89.0)
|
||||
@@ -0,0 +1,223 @@
|
||||
resources:
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: auth
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: auth
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: auth
|
||||
port_value: 9999
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: rest
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: rest
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: rest
|
||||
port_value: 3000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: realtime
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: realtime
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: realtime-dev.supabase-realtime
|
||||
port_value: 4000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: storage
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: storage
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: storage
|
||||
port_value: 5000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /status
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: functions
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: functions
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: functions
|
||||
port_value: 9000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
tcp_health_check: {}
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: meta
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: meta
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: meta
|
||||
port_value: 8080
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /health
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
|
||||
- '@type': type.googleapis.com/envoy.config.cluster.v3.Cluster
|
||||
name: studio
|
||||
connect_timeout: 5s
|
||||
type: STRICT_DNS
|
||||
dns_refresh_rate: 5s
|
||||
dns_failure_refresh_rate:
|
||||
base_interval: 1s
|
||||
max_interval: 1s
|
||||
lb_policy: ROUND_ROBIN
|
||||
load_assignment:
|
||||
cluster_name: studio
|
||||
endpoints:
|
||||
- lb_endpoints:
|
||||
- endpoint:
|
||||
address:
|
||||
socket_address:
|
||||
address: studio
|
||||
port_value: 3000
|
||||
health_checks:
|
||||
- timeout: 2s
|
||||
interval: 5s
|
||||
unhealthy_threshold: 3
|
||||
healthy_threshold: 2
|
||||
http_health_check:
|
||||
path: /project/default
|
||||
circuit_breakers:
|
||||
thresholds:
|
||||
- priority: DEFAULT
|
||||
max_connections: 10000
|
||||
max_pending_requests: 10000
|
||||
max_requests: 10000
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Generate SHA1 base64 hash for Envoy basic auth user list
|
||||
PASSWORD_HASH=$(printf '%s' "${DASHBOARD_PASSWORD}" | openssl sha1 -binary | openssl base64)
|
||||
DASHBOARD_BASIC_AUTH="${DASHBOARD_USERNAME}:{SHA}${PASSWORD_HASH}"
|
||||
|
||||
echo "Generating Envoy configuration..."
|
||||
|
||||
# Process the lds.yaml template with environment variables using sed
|
||||
# Using | as delimiter since JWT tokens contain /
|
||||
sed -e "s|\${ANON_KEY}|${ANON_KEY}|g" \
|
||||
-e "s|\${ANON_KEY_ASYMMETRIC}|${ANON_KEY_ASYMMETRIC}|g" \
|
||||
-e "s|\${SERVICE_ROLE_KEY}|${SERVICE_ROLE_KEY}|g" \
|
||||
-e "s|\${SERVICE_ROLE_KEY_ASYMMETRIC}|${SERVICE_ROLE_KEY_ASYMMETRIC}|g" \
|
||||
-e "s|\${SUPABASE_PUBLISHABLE_KEY}|${SUPABASE_PUBLISHABLE_KEY}|g" \
|
||||
-e "s|\${SUPABASE_SECRET_KEY}|${SUPABASE_SECRET_KEY}|g" \
|
||||
-e "s|\${SUPABASE_PUBLIC_URL}|${SUPABASE_PUBLIC_URL}|g" \
|
||||
-e "s|\${DASHBOARD_BASIC_AUTH}|${DASHBOARD_BASIC_AUTH}|g" \
|
||||
/etc/envoy/lds.template.yaml > /etc/envoy/lds.yaml
|
||||
|
||||
if [ -n "$SUPABASE_SECRET_KEY" ] && \
|
||||
[ -n "$SUPABASE_PUBLISHABLE_KEY" ] && \
|
||||
[ -n "$SERVICE_ROLE_KEY_ASYMMETRIC" ] && \
|
||||
[ -n "$ANON_KEY_ASYMMETRIC" ]; then
|
||||
echo "Envoy sb_ key translation enabled"
|
||||
else
|
||||
echo "Envoy running in legacy API key mode (sb_ keys disabled)"
|
||||
fi
|
||||
|
||||
echo "Envoy configuration generated successfully"
|
||||
echo "Starting Envoy..."
|
||||
|
||||
# Start Envoy
|
||||
exec envoy -c /etc/envoy/envoy.yaml "$@"
|
||||
@@ -0,0 +1,27 @@
|
||||
dynamic_resources:
|
||||
cds_config:
|
||||
path_config_source:
|
||||
path: /etc/envoy/cds.yaml
|
||||
resource_api_version: V3
|
||||
lds_config:
|
||||
path_config_source:
|
||||
path: /etc/envoy/lds.yaml
|
||||
resource_api_version: V3
|
||||
|
||||
node:
|
||||
cluster: supabase_cluster
|
||||
id: supabase_node
|
||||
|
||||
overload_manager:
|
||||
resource_monitors:
|
||||
- name: envoy.resource_monitors.global_downstream_max_connections
|
||||
typed_config:
|
||||
'@type': >-
|
||||
type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig
|
||||
max_active_downstream_connections: 30000
|
||||
|
||||
admin:
|
||||
address:
|
||||
socket_address:
|
||||
address: 127.0.0.1
|
||||
port_value: 9901
|
||||
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
# Custom entrypoint for Kong that builds Lua expressions for request-transformer
|
||||
# and performs environment variable substitution in the declarative config.
|
||||
|
||||
# Build Lua expressions for translating opaque API keys to asymmetric JWTs.
|
||||
# When opaque keys are not configured (empty env vars), expressions fall through
|
||||
# to legacy-only behavior - just passing apikey as-is.
|
||||
#
|
||||
# Full expression logic (when opaque keys are configured):
|
||||
# 1. If Authorization header exists and is NOT an sb_ key -> pass through (user session JWT)
|
||||
# 2. If apikey matches secret key -> set service_role asymmetric JWT internal "API key"
|
||||
# 3. If apikey matches publishable key -> set anon asymmetric JWT internal "API key"
|
||||
# 4. Fallback: pass apikey as-is (legacy HS256 JWT)
|
||||
|
||||
if [ -n "$SUPABASE_SECRET_KEY" ] && [ -n "$SUPABASE_PUBLISHABLE_KEY" ]; then
|
||||
# Opaque keys configured -> full translation expressions
|
||||
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '$SUPABASE_SECRET_KEY' and 'Bearer $SERVICE_ROLE_KEY_ASYMMETRIC') or (headers.apikey == '$SUPABASE_PUBLISHABLE_KEY' and 'Bearer $ANON_KEY_ASYMMETRIC') or headers.apikey)"
|
||||
|
||||
# Realtime WebSocket: reads from query_params.apikey (supabase-js sends apikey
|
||||
# via query string), outputs to x-api-key header which Realtime checks first.
|
||||
export LUA_RT_WS_EXPR="\$((query_params.apikey == '$SUPABASE_SECRET_KEY' and '$SERVICE_ROLE_KEY_ASYMMETRIC') or (query_params.apikey == '$SUPABASE_PUBLISHABLE_KEY' and '$ANON_KEY_ASYMMETRIC') or query_params.apikey)"
|
||||
else
|
||||
# Legacy API keys, not sb_ API keys -> pass apikey through unchanged
|
||||
export LUA_AUTH_EXPR="\$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or headers.apikey)"
|
||||
export LUA_RT_WS_EXPR="\$(query_params.apikey)"
|
||||
fi
|
||||
|
||||
# Substitute environment variables in the Kong declarative config.
|
||||
# Uses awk instead of eval/echo to preserve YAML quoting (eval strips double
|
||||
# quotes, breaking "Header: value" patterns that YAML parses as mappings).
|
||||
awk '{
|
||||
result = ""
|
||||
rest = $0
|
||||
while (match(rest, /\$[A-Za-z_][A-Za-z_0-9]*/)) {
|
||||
varname = substr(rest, RSTART + 1, RLENGTH - 1)
|
||||
if (varname in ENVIRON) {
|
||||
result = result substr(rest, 1, RSTART - 1) ENVIRON[varname]
|
||||
} else {
|
||||
result = result substr(rest, 1, RSTART + RLENGTH - 1)
|
||||
}
|
||||
rest = substr(rest, RSTART + RLENGTH)
|
||||
}
|
||||
print result rest
|
||||
}' /home/kong/temp.yml > "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
# Remove empty key-auth credentials (unconfigured opaque keys)
|
||||
sed -i '/^[[:space:]]*- key:[[:space:]]*$/d' "$KONG_DECLARATIVE_CONFIG"
|
||||
|
||||
exec /entrypoint.sh kong docker-start
|
||||
@@ -0,0 +1,467 @@
|
||||
_format_version: '2.1'
|
||||
_transform: true
|
||||
|
||||
###
|
||||
### Consumers / Users
|
||||
###
|
||||
consumers:
|
||||
- username: DASHBOARD
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_ANON_KEY
|
||||
- key: $SUPABASE_PUBLISHABLE_KEY
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: $SUPABASE_SERVICE_KEY
|
||||
- key: $SUPABASE_SECRET_KEY
|
||||
|
||||
###
|
||||
### Access Control List
|
||||
###
|
||||
acls:
|
||||
- consumer: anon
|
||||
group: anon
|
||||
- consumer: service_role
|
||||
group: admin
|
||||
|
||||
###
|
||||
### Dashboard credentials
|
||||
###
|
||||
basicauth_credentials:
|
||||
- consumer: DASHBOARD
|
||||
username: '$DASHBOARD_USERNAME'
|
||||
password: '$DASHBOARD_PASSWORD'
|
||||
|
||||
###
|
||||
### API Routes
|
||||
###
|
||||
services:
|
||||
## Open Auth routes
|
||||
- name: auth-v1-open
|
||||
_comment: 'Auth: /auth/v1/verify* -> http://auth:9999/verify*'
|
||||
url: http://auth:9999/verify
|
||||
routes:
|
||||
- name: auth-v1-open
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/verify
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-callback
|
||||
_comment: 'Auth: /auth/v1/callback* -> http://auth:9999/callback*'
|
||||
url: http://auth:9999/callback
|
||||
routes:
|
||||
- name: auth-v1-open-callback
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/callback
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-authorize
|
||||
_comment: 'Auth: /auth/v1/authorize* -> http://auth:9999/authorize*'
|
||||
url: http://auth:9999/authorize
|
||||
routes:
|
||||
- name: auth-v1-open-authorize
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1-open-jwks
|
||||
_comment: 'Auth: /auth/v1/.well-known/jwks.json -> http://auth:9999/.well-known/jwks.json'
|
||||
url: http://auth:9999/.well-known/jwks.json
|
||||
routes:
|
||||
- name: auth-v1-open-jwks
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/.well-known/jwks.json
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
- name: auth-v1-open-sso-acs
|
||||
url: "http://auth:9999/sso/saml/acs"
|
||||
routes:
|
||||
- name: auth-v1-open-sso-acs
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/sso/saml/acs
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
- name: auth-v1-open-sso-metadata
|
||||
url: "http://auth:9999/sso/saml/metadata"
|
||||
routes:
|
||||
- name: auth-v1-open-sso-metadata
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/sso/saml/metadata
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Secure Auth routes
|
||||
- name: auth-v1
|
||||
_comment: 'Auth: /auth/v1/* -> http://auth:9999/*'
|
||||
url: http://auth:9999/
|
||||
routes:
|
||||
- name: auth-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## OpenAPI root - admin only
|
||||
- name: rest-v1-openapi
|
||||
_comment: 'PostgREST OpenAPI root: /rest/v1/ -> <http://rest:3000/> (admin only). See <https://github.com/orgs/supabase/discussions/42949>'
|
||||
url: http://rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-openapi-root
|
||||
strip_path: true
|
||||
expression: 'http.path == "/rest/v1/"'
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
|
||||
## Secure PostgREST routes
|
||||
- name: rest-v1
|
||||
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
|
||||
url: http://rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure GraphQL routes
|
||||
- name: graphql-v1
|
||||
_comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql'
|
||||
url: http://rest:3000/rpc/graphql
|
||||
routes:
|
||||
- name: graphql-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /graphql/v1
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Content-Profile: graphql_public"
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Secure Realtime routes
|
||||
- name: realtime-v1-ws
|
||||
_comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/socket
|
||||
protocol: ws
|
||||
routes:
|
||||
- name: realtime-v1-ws
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "x-api-key:$LUA_RT_WS_EXPR"
|
||||
replace:
|
||||
querystring:
|
||||
- "apikey:$LUA_RT_WS_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
# Block access to /realtime/v1/api/openapi
|
||||
- name: realtime-v1-rest-openapi
|
||||
_comment: 'Realtime: /realtime/v1/api/openapi/* -> http://realtime:4000/api/openapi/* (blocked)'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api/openapi
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest-openapi
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api/openapi
|
||||
plugins:
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
|
||||
# Block access to /realtime/v1/api/tenants
|
||||
- name: realtime-v1-rest-tenants
|
||||
_comment: 'Realtime: /realtime/v1/api/tenants/* -> http://realtime:4000/api/tenants/* (blocked)'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api/tenants
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest-tenants
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api/tenants
|
||||
plugins:
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
|
||||
- name: realtime-v1-rest
|
||||
_comment: 'Realtime: /realtime/v1/api/* -> http://realtime:4000/api/*'
|
||||
url: http://realtime-dev.supabase-realtime:4000/api
|
||||
protocol: http
|
||||
routes:
|
||||
- name: realtime-v1-rest
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/api
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
|
||||
## Storage API endpoint (with Authorization header transformation).
|
||||
## No key-auth - S3 protocol requests don't carry an apikey header.
|
||||
##
|
||||
## The request-transformer translates opaque API keys to asymmetric JWTs
|
||||
## and passes through existing Authorization headers (user JWTs, AWS SigV4).
|
||||
## When no Authorization or apikey header is present (S3 presigned URLs),
|
||||
## the Lua expression evaluates to nil which Kong renders as empty string.
|
||||
## The post-function strips this empty header so Storage's S3 signature
|
||||
## verification falls through to query-parameter parsing.
|
||||
- name: storage-v1
|
||||
_comment: 'Storage: /storage/v1/* -> http://storage:5000/*'
|
||||
url: http://storage:5000/
|
||||
routes:
|
||||
- name: storage-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /storage/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: request-transformer
|
||||
config:
|
||||
add:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
replace:
|
||||
headers:
|
||||
- "Authorization: $LUA_AUTH_EXPR"
|
||||
- name: post-function
|
||||
config:
|
||||
access:
|
||||
- |
|
||||
local auth = kong.request.get_header("authorization")
|
||||
if auth == nil or auth == "" or auth:find("^%s*$") then
|
||||
kong.service.request.clear_header("authorization")
|
||||
end
|
||||
|
||||
## Edge Functions routes
|
||||
- name: functions-v1
|
||||
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
|
||||
url: http://functions:9000/
|
||||
read_timeout: 150000
|
||||
routes:
|
||||
- name: functions-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /functions/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## OAuth 2.0 Authorization Server Metadata (RFC 8414)
|
||||
- name: well-known-oauth
|
||||
_comment: 'Auth: /.well-known/oauth-authorization-server -> http://auth:9999/.well-known/oauth-authorization-server'
|
||||
url: http://auth:9999/.well-known/oauth-authorization-server
|
||||
routes:
|
||||
- name: well-known-oauth
|
||||
strip_path: true
|
||||
paths:
|
||||
- /.well-known/oauth-authorization-server
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
## Analytics routes
|
||||
## Not used - Studio and Vector talk directly to analytics via Docker networking.
|
||||
## If external access is needed, add routes with key-auth matching Logflare's x-api-key auth.
|
||||
# - name: analytics-v1-api
|
||||
# _comment: 'Analytics: /analytics/v1/api/endpoints/* -> http://logflare:4000/api/endpoints/*'
|
||||
# url: http://analytics:4000/api/endpoints
|
||||
# routes:
|
||||
# - name: analytics-v1-api
|
||||
# strip_path: true
|
||||
# paths:
|
||||
# - /analytics/v1/api/endpoints/
|
||||
# - name: analytics-v1
|
||||
# _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
|
||||
# url: http://analytics:4000/
|
||||
# routes:
|
||||
# - name: dashboard-v1-all
|
||||
# strip_path: true
|
||||
# paths:
|
||||
# - /analytics/v1
|
||||
# plugins:
|
||||
# - name: cors
|
||||
# - name: basic-auth
|
||||
# config:
|
||||
# hide_credentials: true
|
||||
|
||||
## Secure Database routes
|
||||
- name: meta
|
||||
_comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*'
|
||||
url: http://meta:8080/
|
||||
routes:
|
||||
- name: meta-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /pg/
|
||||
plugins:
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
|
||||
## Block access to /api/mcp
|
||||
- name: mcp-blocker
|
||||
_comment: 'Block direct access to /api/mcp'
|
||||
url: http://studio:3000/api/mcp
|
||||
routes:
|
||||
- name: mcp-blocker-route
|
||||
strip_path: true
|
||||
paths:
|
||||
- /api/mcp
|
||||
plugins:
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
|
||||
## MCP endpoint - local access
|
||||
- name: mcp
|
||||
_comment: 'MCP: /mcp -> http://studio:3000/api/mcp (local access)'
|
||||
url: http://studio:3000/api/mcp
|
||||
routes:
|
||||
- name: mcp
|
||||
strip_path: true
|
||||
paths:
|
||||
- /mcp
|
||||
plugins:
|
||||
# Block access to /mcp by default
|
||||
- name: request-termination
|
||||
config:
|
||||
status_code: 403
|
||||
message: "Access is forbidden."
|
||||
# Enable local access (danger zone!)
|
||||
# 1. Comment out the 'request-termination' section above
|
||||
# 2. Uncomment the entire section below, including 'deny'
|
||||
# 3. Add your local IPs to the 'allow' list
|
||||
#- name: cors
|
||||
#- name: ip-restriction
|
||||
# config:
|
||||
# allow:
|
||||
# - 127.0.0.1
|
||||
# - ::1
|
||||
# deny: []
|
||||
|
||||
## Protected Dashboard - catch all remaining routes
|
||||
- name: dashboard
|
||||
_comment: 'Studio: /* -> http://studio:3000/*'
|
||||
url: http://studio:3000/
|
||||
routes:
|
||||
- name: dashboard-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: basic-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
@@ -0,0 +1,3 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
CREATE DATABASE _supabase WITH OWNER :pguser;
|
||||
@@ -0,0 +1,5 @@
|
||||
\set jwt_secret `echo "$JWT_SECRET"`
|
||||
\set jwt_exp `echo "$JWT_EXP"`
|
||||
|
||||
ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret';
|
||||
ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp';
|
||||
@@ -0,0 +1,6 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
\c _supabase
|
||||
create schema if not exists _analytics;
|
||||
alter schema _analytics owner to :pguser;
|
||||
\c postgres
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user