diff --git a/.env.example b/.env.example index 62e070f..a5c1a10 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,14 @@ # # Die Rolle in diesem String darf KEIN BYPASSRLS haben: fehlt der # Sitzungskontext, sollen die Policies nichts zurückgeben statt alles. +# +# Hinter einem Verbindungspooler (Supabase Supavisor, PgBouncer) den +# TRANSAKTIONS-Modus nehmen, nicht den Sitzungs-Modus — bei Supabase Port +# 6543 statt 5432. Jede Abfrage dieser Anwendung läuft ohnehin in einer +# Transaktion, und der Sitzungskontext wird transaktionslokal gesetzt; beides +# passt genau dazu. Der Sitzungs-Modus belegt dagegen je Client eine feste +# Verbindung und ist bei Supabase auf 15 begrenzt — danach antwortet die +# Anwendung nur noch mit „max clients reached". DATABASE_URL= # Auf "false" setzen, wenn die Datenbank ohne TLS läuft (lokal, CI). DATABASE_SSL= diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index f47312d..efb76d2 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -37,7 +37,7 @@ Werte eintragen: | Variable | Woher | |---|---| -| `DATABASE_URL` | Verbindungsstring der PostgreSQL-Instanz. Die Rolle darf **kein** `BYPASSRLS` haben | +| `DATABASE_URL` | Verbindungsstring der PostgreSQL-Instanz. Die Rolle darf **kein** `BYPASSRLS` haben; hinter einem Pooler den **Transaktions-Modus** (bei Supabase Port 6543) | | `DATABASE_SSL` | nur setzen (`false`), wenn die Datenbank ohne TLS läuft | | `AUTH_SECRET` | selbst generieren: `openssl rand -base64 32` | | `AUTH_MICROSOFT_ENTRA_ID_ID` | Entra-Portal → App-Registrierung → Übersicht | @@ -158,6 +158,9 @@ Single-Instance-Compose-Konfiguration ist das nicht nötig. - **Cron läuft nicht:** `docker compose logs cron` – prüft, ob `/etc/crontabs/root` korrekt geschrieben wurde und ob `CRON_SECRET` in `.env` gesetzt ist (leer/fehlend führt serverseitig zu `401`). +- **`max clients reached in session mode`:** der Verbindungsstring zeigt auf + den Sitzungs-Modus des Poolers. Auf den Transaktions-Modus wechseln (bei + Supabase Port 6543). - **Healthcheck rot:** `docker compose logs app` – meist `DATABASE_URL` fehlend oder nicht erreichbar. Der Pool baut die Verbindung erst beim ersten Zugriff auf, der Fehler steht deshalb im Log der Anfrage, nicht im diff --git a/lib/db/pool.ts b/lib/db/pool.ts index 317e991..f5873b8 100644 --- a/lib/db/pool.ts +++ b/lib/db/pool.ts @@ -45,7 +45,18 @@ types.setTypeParser(1700, (v) => Number(v)); // numeric → Zahl // die Kysely-Instanz, die ihn benutzt, und eine ESLint-Regel verbietet den // Import von `pg` und von dieser Datei überall sonst. -let instance: Pool | undefined; +// Der Pool hängt am globalen Objekt, nicht nur am Modul. +// +// Im Entwicklungsbetrieb lädt Next.js geänderte Module neu. Ein modul-lokales +// `let` wäre danach leer, der alte Pool aber weiterhin am Leben — mit seinen +// Verbindungen. Nach ein paar Bearbeitungen ist das Kontingent des Anbieters +// aufgebraucht, und die Anwendung antwortet nur noch mit „max clients +// reached". Genau so ist sie hier stehengeblieben. +// +// In der Produktion gibt es kein Neuladen; dort ist die Zeile wirkungslos. +const globalForPool = globalThis as typeof globalThis & { __alpenwerkPool?: Pool }; + +let instance: Pool | undefined = globalForPool.__alpenwerkPool; /** * Der Verbindungspool — erst beim ersten Zugriff angelegt, nicht beim Import. @@ -86,5 +97,6 @@ export function getPool(): Pool { console.error("Unerwarteter Fehler auf einer Leerlaufverbindung:", err); }); + globalForPool.__alpenwerkPool = instance; return instance; }