Updated
This commit is contained in:
+144
-96
@@ -1,63 +1,161 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# hackerspace-management environment variables
|
||||
# -----------------------------------------------------------------------------
|
||||
# Copy to .env.local for development; set the same keys on your host for prod.
|
||||
# .env.local is git-ignored. Never commit real secrets.
|
||||
# -----------------------------------------------------------------------------
|
||||
# =============================================================================
|
||||
# .env.example - config for docker-compose.yml (app + local Supabase stack)
|
||||
# =============================================================================
|
||||
# ./scripts/generate-env.sh # copies this file to .env and fills
|
||||
# # every secret below with fresh values
|
||||
# docker compose up -d --build
|
||||
#
|
||||
# For `pnpm dev` against the local stack, copy these into .env.local:
|
||||
# NEXT_PUBLIC_SUPABASE_URL = NEXT_PUBLIC_SUPABASE_URL below
|
||||
# NEXT_PUBLIC_SUPABASE_ANON_KEY = ANON_KEY below
|
||||
# SUPABASE_SERVICE_ROLE_KEY = SERVICE_ROLE_KEY below
|
||||
#
|
||||
# Secrets left empty here are filled by scripts/generate-env.sh. ANON_KEY and
|
||||
# SERVICE_ROLE_KEY are JWTs signed with JWT_SECRET; if you change JWT_SECRET
|
||||
# you must regenerate both. This file is committed: never put real secrets in
|
||||
# it. Keep the generated .env out of git and lock it down: chmod 600 .env
|
||||
# =============================================================================
|
||||
# =============================================================================
|
||||
|
||||
# Supabase (required)
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...replace-with-anon-key
|
||||
# -----------------------------------------------------------------------------
|
||||
# Postgres
|
||||
# -----------------------------------------------------------------------------
|
||||
# Superuser password for Postgres. Also used by auth/rest/realtime/meta to
|
||||
# connect. Filled by scripts/generate-env.sh (hex, so it is safe inside postgres:// URLs).
|
||||
POSTGRES_PASSWORD=
|
||||
# Database name. Keep "postgres" unless you have a reason not to.
|
||||
POSTGRES_DB=postgres
|
||||
# Host port the Postgres admin port is published on (bound to 127.0.0.1 only,
|
||||
# for psql / pg_dump from the host). Container always listens on 5432.
|
||||
POSTGRES_HOST_PORT=5432
|
||||
|
||||
# Server-side admin key. Bypasses RLS. Required for createSpace / joinSpace.
|
||||
# Treat as a secret. Never expose to the browser. Never log it.
|
||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...replace-with-service-role-key
|
||||
# -----------------------------------------------------------------------------
|
||||
# JWT / API keys (the god-mode secrets)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Symmetric secret GoTrue/PostgREST/Realtime use to sign and verify JWTs.
|
||||
# 32+ random bytes. Generate: openssl rand -hex 32
|
||||
JWT_SECRET=
|
||||
# Access-token lifetime in seconds (1 hour). Also used as the JWT expiry hint.
|
||||
JWT_EXPIRY=3600
|
||||
|
||||
# App URL (required)
|
||||
# Public-facing base URL of this deployment. Used in OAuth redirects and in every
|
||||
# absolute link the app emits (dues emails, form-result links, the /me portal).
|
||||
# Set this to your own domain: it replaces the old hackerspace.sh fallback, so a
|
||||
# single-tenant instance never leaks the platform domain. No trailing slash.
|
||||
# Public "anon" API key. A JWT with role=anon signed by JWT_SECRET above.
|
||||
# This is the NEXT_PUBLIC_SUPABASE_ANON_KEY the app/browser use. Generate with
|
||||
# JWT_SECRET exported, on your local machine:
|
||||
# export JWT_SECRET='the-value-above'
|
||||
# npx -y -p jsonwebtoken@9 node -e "const jwt=require('jsonwebtoken');const iat=Math.floor(Date.now()/1000);console.log(jwt.sign({role:'anon',iss:'supabase',iat,exp:iat+60*60*24*365*10},process.env.JWT_SECRET))"
|
||||
ANON_KEY=
|
||||
|
||||
# Service-role API key. A JWT with role=service_role signed by JWT_SECRET.
|
||||
# Bypasses RLS (god mode) - this is the app's SUPABASE_SERVICE_ROLE_KEY. Same
|
||||
# command as above with role:'service_role'. Treat like root credentials.
|
||||
# npx -y -p jsonwebtoken@9 node -e "const jwt=require('jsonwebtoken');const iat=Math.floor(Date.now()/1000);console.log(jwt.sign({role:'service_role',iss:'supabase',iat,exp:iat+60*60*24*365*10},process.env.JWT_SECRET))"
|
||||
SERVICE_ROLE_KEY=
|
||||
|
||||
# Realtime needs its own long random base for signing/cookies. Independent of
|
||||
# JWT_SECRET. Generate: openssl rand -hex 32
|
||||
SECRET_KEY_BASE=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public URLs
|
||||
# -----------------------------------------------------------------------------
|
||||
# Where GoTrue sends users back after auth flows, and the app's own public URL.
|
||||
# For a local bundle these are localhost; for a server put your real domains
|
||||
# here (and terminate TLS in a reverse proxy in front of Kong / the app).
|
||||
SITE_URL=http://localhost:3000
|
||||
# Comma-separated extra redirect URLs GoTrue will accept.
|
||||
ADDITIONAL_REDIRECT_URLS=http://localhost:3000
|
||||
# The externally reachable base URL of the Supabase API gateway (Kong). GoTrue
|
||||
# embeds this in email links. Local bundle: http://localhost:8000
|
||||
API_EXTERNAL_URL=http://localhost:8000
|
||||
# Same gateway URL, shown by Studio. Usually identical to API_EXTERNAL_URL.
|
||||
SUPABASE_PUBLIC_URL=http://localhost:8000
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Kong (API gateway) host ports (bound to 127.0.0.1 by the compose file)
|
||||
# -----------------------------------------------------------------------------
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Studio (admin dashboard) (bound to 127.0.0.1 by the compose file)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Host port Studio is published on. Keep it off 3000 (the app owns that).
|
||||
STUDIO_PORT=3001
|
||||
STUDIO_DEFAULT_ORGANIZATION=hackerspace
|
||||
STUDIO_DEFAULT_PROJECT=hackerspace
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PostgREST
|
||||
# -----------------------------------------------------------------------------
|
||||
# Schemas PostgREST exposes over /rest/v1. The app only needs public; the rest
|
||||
# match the Supabase defaults.
|
||||
PGRST_DB_SCHEMAS=public,storage,graphql_public
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Auth behavior
|
||||
# -----------------------------------------------------------------------------
|
||||
# Allow email/password signup (the app's join flow needs this).
|
||||
ENABLE_EMAIL_SIGNUP=true
|
||||
# Auto-confirm new emails WITHOUT sending a verification mail. Handy for local
|
||||
# dev when you have no SMTP configured; set false in production.
|
||||
ENABLE_EMAIL_AUTOCONFIRM=true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SMTP for auth emails (invites, confirmations, password resets).
|
||||
# Optional for local dev if ENABLE_EMAIL_AUTOCONFIRM=true. Required in prod.
|
||||
# Use any provider (Resend, SendGrid, SES, Postmark, Mailgun).
|
||||
# -----------------------------------------------------------------------------
|
||||
SMTP_ADMIN_EMAIL=admin@example.com
|
||||
SMTP_HOST=smtp.resend.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=resend
|
||||
SMTP_PASS=
|
||||
SMTP_SENDER_NAME=hackerspace.sh
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# The app (Next.js) - these feed both build args and runtime env.
|
||||
# -----------------------------------------------------------------------------
|
||||
# Browser-reachable Supabase (Kong) URL. Baked into the client bundle at build
|
||||
# time, so it must be what the BROWSER can reach: http://localhost:8000 locally,
|
||||
# or https://supabase.yourdomain.com behind a proxy. Keep it equal to
|
||||
# API_EXTERNAL_URL. (The app's anon key = ANON_KEY above; service key =
|
||||
# SERVICE_ROLE_KEY above - the compose wires those through automatically.)
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
|
||||
# Public base URL of the app itself (OAuth redirects, emailed links). No
|
||||
# trailing slash.
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Single-tenant mode (optional). See docs/SINGLE_TENANT.md for the full guide.
|
||||
# Single-tenant mode (optional; see docs/SINGLE_TENANT.md).
|
||||
# Leave blank for the default multi-tenant platform. Set NEXT_PUBLIC_SINGLE_TENANT=true
|
||||
# to dedicate this instance to exactly one space.
|
||||
# -----------------------------------------------------------------------------
|
||||
# Leave everything below unset to run the default MULTI-TENANT platform (one
|
||||
# deployment hosts many spaces, users create/join spaces at signup — the
|
||||
# hackerspace.sh model). Set NEXT_PUBLIC_SINGLE_TENANT=true to run a dedicated
|
||||
# instance for exactly ONE space:
|
||||
# - the "create a space" flow is disabled (server + UI),
|
||||
# - signup becomes "join THE space",
|
||||
# - the hackerspace.sh marketing/landing shell is hidden.
|
||||
# These are NEXT_PUBLIC_ because the signup page (a client component) reads them;
|
||||
# the server re-enforces every rule, so they are safe to expose.
|
||||
NEXT_PUBLIC_SINGLE_TENANT=
|
||||
|
||||
# Brand name shown throughout the UI (logo wordmark, page titles). White-label
|
||||
# your instance here. Defaults to "hackerspace.sh" when unset.
|
||||
NEXT_PUBLIC_SITE_NAME=
|
||||
|
||||
# The slug of your one space (matches spaces.slug, created by `pnpm setup`).
|
||||
# In single-tenant mode new signups join this space. If unset, the app falls
|
||||
# back to "the only space in the database".
|
||||
NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG=
|
||||
|
||||
# Set to "true" to let people sign up and join your space WITHOUT an invite code
|
||||
# (still subject to the space's require_approval setting — approve them from the
|
||||
# members page). Leave unset to require an invite code even in single-tenant mode.
|
||||
NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN=
|
||||
|
||||
# Override the marketing shell visibility. Defaults to ON for the platform and
|
||||
# OFF in single-tenant mode. Set "true" to keep the /resources subsite + landing
|
||||
# page, or "false" to hide them on a multi-tenant deploy.
|
||||
NEXT_PUBLIC_SHOW_MARKETING=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# First-admin bootstrap (used ONLY by `pnpm setup`, never read at runtime).
|
||||
# `pnpm setup` provisions your one space and creates the first admin login from
|
||||
# these values (or from interactive prompts / CLI flags). Fill them in, run the
|
||||
# CLI once, then you can clear the password. See docs/SINGLE_TENANT.md.
|
||||
# App secrets and optional integrations
|
||||
# -----------------------------------------------------------------------------
|
||||
# Secrets vault encryption key (/ops Secrets feature): 64 hex chars. Filled by
|
||||
# scripts/generate-env.sh. Rotating it invalidates existing ciphertexts.
|
||||
SECRETS_ENCRYPTION_KEY=
|
||||
# Bearer secret guarding /api/cron/notifications. Filled by the script.
|
||||
CRON_SECRET=
|
||||
|
||||
# Transactional email (optional). Unset = outbox rows are recorded as failed.
|
||||
RESEND_API_KEY=
|
||||
EMAIL_FROM=
|
||||
# Sentry-compatible DSN (optional). Unset = error capture no-ops.
|
||||
SENTRY_DSN=
|
||||
# Login-page OAuth buttons: "true" only for providers configured in GoTrue.
|
||||
NEXT_PUBLIC_OAUTH_GITHUB=
|
||||
NEXT_PUBLIC_OAUTH_GOOGLE=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# First-admin bootstrap (read only by `pnpm setup`, never at runtime).
|
||||
# -----------------------------------------------------------------------------
|
||||
SETUP_SPACE_NAME=
|
||||
SETUP_SPACE_SLUG=
|
||||
@@ -65,53 +163,3 @@ SETUP_SPACE_CITY=
|
||||
SETUP_ADMIN_EMAIL=
|
||||
SETUP_ADMIN_PASSWORD=
|
||||
SETUP_ADMIN_NAME=
|
||||
|
||||
# Secrets vault encryption key (required for the /ops Secrets feature).
|
||||
# 32 bytes of cryptographically random data, hex-encoded (64 hex chars).
|
||||
# Generate with: openssl rand -hex 32
|
||||
# Without this set, the secrets feature falls back to legacy plain-text storage.
|
||||
# Rotating this key invalidates all existing ciphertexts; re-enter each secret.
|
||||
SECRETS_ENCRYPTION_KEY=
|
||||
|
||||
# OAuth provider buttons on the login page (optional).
|
||||
# Set to "true" only for a provider you have actually configured in Supabase
|
||||
# Auth. Unset/anything-else hides that button (an unconfigured provider button
|
||||
# just dead-ends in a Supabase error). If neither is "true" the whole
|
||||
# "OR / social sign-in" block is hidden.
|
||||
NEXT_PUBLIC_OAUTH_GITHUB=
|
||||
NEXT_PUBLIC_OAUTH_GOOGLE=
|
||||
|
||||
# Node env (optional, defaults to "development" locally)
|
||||
NODE_ENV=development
|
||||
|
||||
# Transactional email (optional; required for dues-lifecycle notifications).
|
||||
# Resend HTTP API key (https://resend.com, starts with "re_"). Unset = the
|
||||
# notification outbox still fills but the dispatcher records each row as
|
||||
# failed with "transport not configured" instead of sending.
|
||||
RESEND_API_KEY=
|
||||
# From address for all platform email. Must be on a domain you have verified
|
||||
# in Resend (SPF + DKIM) for production delivery. Friendly name optional.
|
||||
# Example: HeatSync Labs <noreply@hackerspace.sh>
|
||||
EMAIL_FROM=
|
||||
# Shared secret guarding the notification dispatcher (/api/cron/notifications).
|
||||
# The droplet's crontab POSTs with "Authorization: Bearer $CRON_SECRET" once a
|
||||
# minute. Unset = the dispatcher returns 503 and never sends. Generate with:
|
||||
# openssl rand -hex 32
|
||||
CRON_SECRET=
|
||||
|
||||
# Error monitoring (optional). Sentry-format DSN for a self-hosted GlitchTip
|
||||
# (or any Sentry-compatible) ingest endpoint. Unset = the capture seam no-ops
|
||||
# (no events sent), so the app ships dark until the backend is provisioned.
|
||||
# Server-side capture only (money path, crons, server actions); no client SDK.
|
||||
# Example: https://<publicKey>@glitchtip.hackerspace.sh/1
|
||||
SENTRY_DSN=
|
||||
|
||||
# Docker build flag (only when building the standalone image)
|
||||
# Set to "1" in the Dockerfile so next.config.mjs emits standalone output.
|
||||
# DOCKER_BUILD=1
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Per-space integration credentials (PayPal, Zeffy, Venmo, Stripe) are stored
|
||||
# in the database under the `integrations` table, not in env. The
|
||||
# /api/paypal/sync endpoint reads them from there.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
# =============================================================================
|
||||
# .env.full.example - config for docker-compose.full.yml (the convenience
|
||||
# bundle: app + self-hosted Supabase in one command).
|
||||
#
|
||||
# cp .env.full.example .env.full
|
||||
# # fill in the secrets below, then:
|
||||
# docker compose -f docker-compose.full.yml --env-file .env.full up -d
|
||||
#
|
||||
# Every variable docker-compose.full.yml references is documented here. This is
|
||||
# the fast path. For the hardened, internet-facing deployment (Nginx + TLS,
|
||||
# backups, Studio behind auth) follow docs/DEPLOY_DO_SELFHOSTED.md; that guide
|
||||
# is the source of truth for real deployments.
|
||||
#
|
||||
# The four critical secrets are POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, and
|
||||
# SERVICE_ROLE_KEY. ANON_KEY and SERVICE_ROLE_KEY are JWTs SIGNED WITH the same
|
||||
# JWT_SECRET you set below: if you change JWT_SECRET you MUST regenerate both.
|
||||
# Never commit .env.full. Lock it down: chmod 600 .env.full
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Postgres
|
||||
# -----------------------------------------------------------------------------
|
||||
# Superuser password for Postgres. Also used by auth/rest/realtime/meta to
|
||||
# connect. Generate: openssl rand -base64 24
|
||||
POSTGRES_PASSWORD=replace-with-openssl-rand-base64-24
|
||||
# Database name. Keep "postgres" unless you have a reason not to.
|
||||
POSTGRES_DB=postgres
|
||||
# Host port the Postgres admin port is published on (bound to 127.0.0.1 only,
|
||||
# for psql / pg_dump from the host). Container always listens on 5432.
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# JWT / API keys (the god-mode secrets)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Symmetric secret GoTrue/PostgREST/Realtime use to sign and verify JWTs.
|
||||
# 32+ random bytes. Generate: openssl rand -hex 32
|
||||
JWT_SECRET=replace-with-openssl-rand-hex-32
|
||||
# Access-token lifetime in seconds (1 hour). Also used as the JWT expiry hint.
|
||||
JWT_EXPIRY=3600
|
||||
|
||||
# Public "anon" API key. A JWT with role=anon signed by JWT_SECRET above.
|
||||
# This is the NEXT_PUBLIC_SUPABASE_ANON_KEY the app/browser use. Generate with
|
||||
# JWT_SECRET exported, on your local machine:
|
||||
# export JWT_SECRET='the-value-above'
|
||||
# npx -y -p jsonwebtoken@9 node -e "const jwt=require('jsonwebtoken');const iat=Math.floor(Date.now()/1000);console.log(jwt.sign({role:'anon',iss:'supabase',iat,exp:iat+60*60*24*365*10},process.env.JWT_SECRET))"
|
||||
ANON_KEY=replace-with-anon-jwt-signed-by-JWT_SECRET
|
||||
|
||||
# Service-role API key. A JWT with role=service_role signed by JWT_SECRET.
|
||||
# Bypasses RLS (god mode) - this is the app's SUPABASE_SERVICE_ROLE_KEY. Same
|
||||
# command as above with role:'service_role'. Treat like root credentials.
|
||||
# npx -y -p jsonwebtoken@9 node -e "const jwt=require('jsonwebtoken');const iat=Math.floor(Date.now()/1000);console.log(jwt.sign({role:'service_role',iss:'supabase',iat,exp:iat+60*60*24*365*10},process.env.JWT_SECRET))"
|
||||
SERVICE_ROLE_KEY=replace-with-service-role-jwt-signed-by-JWT_SECRET
|
||||
|
||||
# Realtime needs its own long random base for signing/cookies. Independent of
|
||||
# JWT_SECRET. Generate: openssl rand -hex 32
|
||||
SECRET_KEY_BASE=replace-with-openssl-rand-hex-32
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public URLs
|
||||
# -----------------------------------------------------------------------------
|
||||
# Where GoTrue sends users back after auth flows, and the app's own public URL.
|
||||
# For a local bundle these are localhost; for a server put your real domains
|
||||
# here (and terminate TLS in a reverse proxy in front of Kong / the app).
|
||||
SITE_URL=http://localhost:3000
|
||||
# Comma-separated extra redirect URLs GoTrue will accept.
|
||||
ADDITIONAL_REDIRECT_URLS=http://localhost:3000
|
||||
# The externally reachable base URL of the Supabase API gateway (Kong). GoTrue
|
||||
# embeds this in email links. Local bundle: http://localhost:8000
|
||||
API_EXTERNAL_URL=http://localhost:8000
|
||||
# Same gateway URL, shown by Studio. Usually identical to API_EXTERNAL_URL.
|
||||
SUPABASE_PUBLIC_URL=http://localhost:8000
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Kong (API gateway) host ports (bound to 127.0.0.1 by the compose file)
|
||||
# -----------------------------------------------------------------------------
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Studio (admin dashboard) (bound to 127.0.0.1 by the compose file)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Host port Studio is published on. Keep it off 3000 (the app owns that).
|
||||
STUDIO_PORT=3001
|
||||
STUDIO_DEFAULT_ORGANIZATION=hackerspace
|
||||
STUDIO_DEFAULT_PROJECT=hackerspace
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# PostgREST
|
||||
# -----------------------------------------------------------------------------
|
||||
# Schemas PostgREST exposes over /rest/v1. The app only needs public; the rest
|
||||
# match the Supabase defaults.
|
||||
PGRST_DB_SCHEMAS=public,storage,graphql_public
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Auth behavior
|
||||
# -----------------------------------------------------------------------------
|
||||
# Allow email/password signup (the app's join flow needs this).
|
||||
ENABLE_EMAIL_SIGNUP=true
|
||||
# Auto-confirm new emails WITHOUT sending a verification mail. Handy for local
|
||||
# dev when you have no SMTP configured; set false in production.
|
||||
ENABLE_EMAIL_AUTOCONFIRM=true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SMTP for auth emails (invites, confirmations, password resets).
|
||||
# Optional for local dev if ENABLE_EMAIL_AUTOCONFIRM=true. Required in prod.
|
||||
# Use any provider (Resend, SendGrid, SES, Postmark, Mailgun).
|
||||
# -----------------------------------------------------------------------------
|
||||
SMTP_ADMIN_EMAIL=admin@example.com
|
||||
SMTP_HOST=smtp.resend.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=resend
|
||||
SMTP_PASS=replace-with-smtp-api-key
|
||||
SMTP_SENDER_NAME=hackerspace.sh
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# The app (Next.js) - these feed both build args and runtime env.
|
||||
# -----------------------------------------------------------------------------
|
||||
# Browser-reachable Supabase (Kong) URL. Baked into the client bundle at build
|
||||
# time, so it must be what the BROWSER can reach: http://localhost:8000 locally,
|
||||
# or https://supabase.yourdomain.com behind a proxy. Keep it equal to
|
||||
# API_EXTERNAL_URL. (The app's anon key = ANON_KEY above; service key =
|
||||
# SERVICE_ROLE_KEY above - the compose wires those through automatically.)
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://localhost:8000
|
||||
# Public base URL of the app itself (OAuth redirects, emailed links). No
|
||||
# trailing slash.
|
||||
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Single-tenant mode (optional; see docs/SINGLE_TENANT.md).
|
||||
# Leave blank for the default multi-tenant platform. Set NEXT_PUBLIC_SINGLE_TENANT=true
|
||||
# to dedicate this instance to exactly one space.
|
||||
# -----------------------------------------------------------------------------
|
||||
NEXT_PUBLIC_SINGLE_TENANT=
|
||||
NEXT_PUBLIC_SITE_NAME=
|
||||
NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG=
|
||||
NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN=
|
||||
NEXT_PUBLIC_SHOW_MARKETING=
|
||||
@@ -6,6 +6,7 @@ __v0_jsx-dev-runtime.ts
|
||||
# Common ignores
|
||||
node_modules/
|
||||
.next/
|
||||
.env
|
||||
.env*.local
|
||||
.DS_Store
|
||||
|
||||
|
||||
+11
-1
@@ -15,7 +15,7 @@ WORKDIR /app
|
||||
|
||||
# ----- deps ------------------------------------------------------------------
|
||||
FROM base AS deps
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# ----- builder ---------------------------------------------------------------
|
||||
@@ -33,9 +33,19 @@ ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ARG NEXT_PUBLIC_SUPABASE_URL
|
||||
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
ARG NEXT_PUBLIC_APP_URL
|
||||
ARG NEXT_PUBLIC_SINGLE_TENANT
|
||||
ARG NEXT_PUBLIC_SITE_NAME
|
||||
ARG NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG
|
||||
ARG NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN
|
||||
ARG NEXT_PUBLIC_SHOW_MARKETING
|
||||
ENV NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}
|
||||
ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}
|
||||
ENV NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL}
|
||||
ENV NEXT_PUBLIC_SINGLE_TENANT=${NEXT_PUBLIC_SINGLE_TENANT}
|
||||
ENV NEXT_PUBLIC_SITE_NAME=${NEXT_PUBLIC_SITE_NAME}
|
||||
ENV NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG=${NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG}
|
||||
ENV NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN=${NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN}
|
||||
ENV NEXT_PUBLIC_SHOW_MARKETING=${NEXT_PUBLIC_SHOW_MARKETING}
|
||||
|
||||
RUN pnpm build
|
||||
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ pnpm build
|
||||
# ---------------------------------------------------------------------------
|
||||
log "restarting the app"
|
||||
if [[ -f "$APP_DIR/Dockerfile" ]] && command -v docker >/dev/null 2>&1; then
|
||||
docker compose up -d --build "$APP_SERVICE"
|
||||
docker compose -f docker-compose.app.yml up -d --build "$APP_SERVICE"
|
||||
elif command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl restart "$SYSTEMD_APP_UNIT"
|
||||
else
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Production-style local deploy of the hackerspace-management app.
|
||||
# Reads secrets from .env.local. For a real droplet, point env_file at
|
||||
# .env.production and lock the file to 0600.
|
||||
# -----------------------------------------------------------------------------
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
|
||||
NEXT_PUBLIC_SINGLE_TENANT: ${NEXT_PUBLIC_SINGLE_TENANT:-}
|
||||
NEXT_PUBLIC_SITE_NAME: ${NEXT_PUBLIC_SITE_NAME:-}
|
||||
NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG: ${NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG:-}
|
||||
NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN: ${NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN:-}
|
||||
NEXT_PUBLIC_SHOW_MARKETING: ${NEXT_PUBLIC_SHOW_MARKETING:-}
|
||||
image: hackerspace-management:latest
|
||||
container_name: hackerspace_app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file:
|
||||
- .env.local
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:3000/api/health",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: hackerspace_net
|
||||
@@ -1,464 +0,0 @@
|
||||
# =============================================================================
|
||||
# docker-compose.full.yml - CONVENIENCE BUNDLE (app + self-hosted Supabase)
|
||||
# =============================================================================
|
||||
# One command brings up the Next.js app PLUS a self-hosted Supabase stack
|
||||
# (Postgres, GoTrue auth, PostgREST, Realtime, the Kong API gateway, and
|
||||
# Studio) so a single-tenant deployer gets application + database together:
|
||||
#
|
||||
# cp .env.full.example .env.full # then fill in the secrets
|
||||
# docker compose -f docker-compose.full.yml --env-file .env.full up -d
|
||||
#
|
||||
# This is the fast path for a laptop, a demo box, or a small single-tenant
|
||||
# instance. It is deliberately opinionated and NOT hardened:
|
||||
# - no Nginx / TLS termination (add your own reverse proxy for the internet),
|
||||
# - no Storage / Edge Functions / analytics (not needed to run this app),
|
||||
# - Kong's declarative config is generated inline instead of from a vendored
|
||||
# file, so everything lives in these two files.
|
||||
#
|
||||
# For the production, hardened, internet-facing deployment (separate networks,
|
||||
# Nginx + Let's Encrypt, backups, Studio behind basic-auth, the full Supabase
|
||||
# compose) follow docs/DEPLOY_DO_SELFHOSTED.md. That guide is the source of
|
||||
# truth for real deployments; this bundle is for convenience only.
|
||||
#
|
||||
# After the stack is healthy you still apply the app schema exactly once
|
||||
# (see docs/DEPLOY_DO_SELFHOSTED.md section 9):
|
||||
# docker compose -f docker-compose.full.yml --env-file .env.full \
|
||||
# exec -T db psql -U postgres -d "$POSTGRES_DB" < scripts/schema.sql
|
||||
# # then each numbered scripts/0NN_*.sql, then: ... restart rest
|
||||
# =============================================================================
|
||||
|
||||
name: hackerspace-full
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Postgres - the one durable service. Everything else is stateless.
|
||||
# supabase/postgres ships the roles (anon, authenticated, service_role,
|
||||
# authenticator, supabase_admin, supabase_auth_admin, ...) and the logical
|
||||
# replication config that GoTrue / PostgREST / Realtime expect on first boot.
|
||||
# ---------------------------------------------------------------------------
|
||||
db:
|
||||
image: supabase/postgres:15.8.1.060
|
||||
container_name: hackerspace_db
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- wal_level=logical
|
||||
- -c
|
||||
- max_replication_slots=5
|
||||
- -c
|
||||
- max_wal_senders=5
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_PORT: "5432"
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_EXP: ${JWT_EXPIRY}
|
||||
ports:
|
||||
# Admin port: bound to localhost only (psql / pg_dump from the host).
|
||||
- "127.0.0.1:${POSTGRES_PORT}:5432"
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -h localhost -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 30s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GoTrue (auth) - /auth/v1/* behind Kong.
|
||||
# ---------------------------------------------------------------------------
|
||||
auth:
|
||||
image: supabase/gotrue:v2.177.0
|
||||
container_name: hackerspace_auth
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
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}@db:5432/${POSTGRES_DB}
|
||||
|
||||
GOTRUE_SITE_URL: ${SITE_URL}
|
||||
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
|
||||
GOTRUE_DISABLE_SIGNUP: "false"
|
||||
|
||||
GOTRUE_JWT_ADMIN_ROLES: service_role
|
||||
GOTRUE_JWT_AUD: authenticated
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
|
||||
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
|
||||
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
|
||||
GOTRUE_EXTERNAL_PHONE_ENABLED: "false"
|
||||
GOTRUE_SMS_AUTOCONFIRM: "false"
|
||||
|
||||
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: /auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:9999/health",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PostgREST (rest) - /rest/v1/* behind Kong.
|
||||
# The postgrest image is a distroless Haskell binary with no shell/curl, so
|
||||
# it cannot self-healthcheck. Readiness for the app is therefore gated on
|
||||
# Kong (which fronts rest); see the app service's depends_on below.
|
||||
# ---------------------------------------------------------------------------
|
||||
rest:
|
||||
image: postgrest/postgrest:v12.2.12
|
||||
container_name: hackerspace_rest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
PGRST_JWT_SECRET: ${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 - /realtime/v1/* behind Kong. The container name is significant:
|
||||
# Kong routes to realtime-dev.supabase-realtime, matching upstream Supabase.
|
||||
# ---------------------------------------------------------------------------
|
||||
realtime:
|
||||
image: supabase/realtime:v2.34.47
|
||||
container_name: realtime-dev.supabase-realtime
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PORT: "4000"
|
||||
DB_HOST: db
|
||||
DB_PORT: "5432"
|
||||
DB_USER: supabase_admin
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime"
|
||||
DB_ENC_KEY: supabaserealtime
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
DNS_NODES: "''"
|
||||
RLIMIT_NOFILE: "10000"
|
||||
APP_NAME: realtime
|
||||
SEED_SELF_HOST: "true"
|
||||
RUN_JANITOR: "true"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"curl",
|
||||
"-sSfL",
|
||||
"--head",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
"http://localhost:4000/api/tenants/realtime-dev/health",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# postgres-meta - backs Studio's table editor. Not fronted by Kong here.
|
||||
# ---------------------------------------------------------------------------
|
||||
meta:
|
||||
image: supabase/postgres-meta:v0.91.0
|
||||
container_name: hackerspace_meta
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PG_META_PORT: "8080"
|
||||
PG_META_DB_HOST: db
|
||||
PG_META_DB_PORT: "5432"
|
||||
PG_META_DB_NAME: ${POSTGRES_DB}
|
||||
PG_META_DB_USER: supabase_admin
|
||||
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kong - the API gateway. Everything the app / browser talks to goes here:
|
||||
# /auth/v1/* -> auth:9999
|
||||
# /rest/v1/* -> rest:3000
|
||||
# /realtime/v1/* -> realtime:4000
|
||||
# Declarative (DB-less) config is generated inline from the ANON_KEY /
|
||||
# SERVICE_ROLE_KEY env vars so this bundle needs no vendored kong.yml.
|
||||
# NOTE: $$VAR below is a literal `$VAR` after compose interpolation; the
|
||||
# heredoc runs in the container shell, which expands it from Kong's env.
|
||||
# ---------------------------------------------------------------------------
|
||||
kong:
|
||||
image: kong:2.8.1
|
||||
container_name: hackerspace_kong
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
auth:
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
realtime:
|
||||
condition: service_started
|
||||
ports:
|
||||
# API gateway. Bound to localhost: a reverse proxy (or the browser on the
|
||||
# same host) reaches it here. Do NOT expose 0.0.0.0 on the public internet
|
||||
# without TLS in front - see docs/DEPLOY_DO_SELFHOSTED.md.
|
||||
- "127.0.0.1:${KONG_HTTP_PORT}:8000"
|
||||
- "127.0.0.1:${KONG_HTTPS_PORT}:8443"
|
||||
environment:
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
KONG_PLUGINS: request-transformer,cors,key-auth,acl
|
||||
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
||||
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
entrypoint:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
cat > /home/kong/kong.yml <<EOF
|
||||
_format_version: '2.1'
|
||||
_transform: true
|
||||
|
||||
consumers:
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: $$SUPABASE_ANON_KEY
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: $$SUPABASE_SERVICE_KEY
|
||||
|
||||
acls:
|
||||
- consumer: anon
|
||||
group: anon
|
||||
- consumer: service_role
|
||||
group: admin
|
||||
|
||||
services:
|
||||
- name: auth-v1-open
|
||||
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
|
||||
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
|
||||
url: http://auth:9999/authorize
|
||||
routes:
|
||||
- name: auth-v1-open-authorize
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1
|
||||
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: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
- name: rest-v1
|
||||
url: http://rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
- name: realtime-v1
|
||||
url: http://realtime-dev.supabase-realtime:4000/socket/
|
||||
routes:
|
||||
- name: realtime-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
EOF
|
||||
exec /docker-entrypoint.sh kong docker-start
|
||||
healthcheck:
|
||||
test: ["CMD", "kong", "health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Studio - the admin dashboard. Bound to localhost only; put it behind a
|
||||
# reverse proxy with auth before exposing it (see the DEPLOY doc, section 11).
|
||||
# ---------------------------------------------------------------------------
|
||||
studio:
|
||||
image: supabase/studio:2025.06.30-sha-6f5982d
|
||||
container_name: hackerspace_studio
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
kong:
|
||||
condition: service_healthy
|
||||
meta:
|
||||
condition: service_started
|
||||
ports:
|
||||
# Admin dashboard: localhost only.
|
||||
- "127.0.0.1:${STUDIO_PORT}:3000"
|
||||
environment:
|
||||
STUDIO_PG_META_URL: http://meta:8080
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
|
||||
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://localhost:3000/api/platform/profile').then((r)=>{if(r.status!==200)throw new Error(r.status)}).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The app - built from the existing Dockerfile. NEXT_PUBLIC_* values are
|
||||
# inlined at build time (Next.js), so they are passed as build args AND at
|
||||
# runtime. Point NEXT_PUBLIC_SUPABASE_URL at the browser-reachable Kong URL
|
||||
# (http://localhost:8000 for a local bundle).
|
||||
# ---------------------------------------------------------------------------
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
|
||||
image: hackerspace-management:latest
|
||||
container_name: hackerspace_app
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
# App waits until the API gateway is healthy. Kong fronts rest/auth/
|
||||
# realtime, so a healthy Kong means the data plane is reachable. rest has
|
||||
# no in-image shell to self-healthcheck, so it is gated here as started
|
||||
# and effectively covered by Kong's own upstream readiness.
|
||||
kong:
|
||||
condition: service_healthy
|
||||
auth:
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
|
||||
# Single-tenant knobs (optional; see .env.full.example / docs/SINGLE_TENANT.md)
|
||||
NEXT_PUBLIC_SINGLE_TENANT: ${NEXT_PUBLIC_SINGLE_TENANT}
|
||||
NEXT_PUBLIC_SITE_NAME: ${NEXT_PUBLIC_SITE_NAME}
|
||||
NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG: ${NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG}
|
||||
NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN: ${NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN}
|
||||
NEXT_PUBLIC_SHOW_MARKETING: ${NEXT_PUBLIC_SHOW_MARKETING}
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:3000/api/health",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
# Postgres data survives `docker compose down`. Use `down -v` to wipe it.
|
||||
db-data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: hackerspace_full_net
|
||||
+447
-8
@@ -1,26 +1,461 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# Production-style local deploy of the hackerspace-management app.
|
||||
# Reads secrets from .env.local. For a real droplet, point env_file at
|
||||
# .env.production and lock the file to 0600.
|
||||
# -----------------------------------------------------------------------------
|
||||
# =============================================================================
|
||||
# docker-compose.yml - local all-in-one stack (app + self-hosted Supabase)
|
||||
# =============================================================================
|
||||
# One command brings up the Next.js app PLUS a self-hosted Supabase stack
|
||||
# (Postgres, GoTrue auth, PostgREST, Realtime, the Kong API gateway, and
|
||||
# Studio), so the database runs locally in containers:
|
||||
#
|
||||
# ./scripts/generate-env.sh # writes .env with fresh secrets
|
||||
# docker compose up -d --build
|
||||
#
|
||||
# Compose reads .env automatically. It is deliberately opinionated and NOT
|
||||
# hardened:
|
||||
# - no Nginx / TLS termination (add your own reverse proxy for the internet),
|
||||
# - no Storage / Edge Functions / analytics (not needed to run this app),
|
||||
# - Kong's declarative config is generated inline instead of from a vendored
|
||||
# file, so everything lives in this one file.
|
||||
#
|
||||
# The app-only file used for the hardened production deploy (separate Supabase
|
||||
# stack) is docker-compose.app.yml; see docs/DEPLOY_DO_SELFHOSTED.md.
|
||||
#
|
||||
# After the stack is healthy, apply the app schema exactly once:
|
||||
# docker compose exec -T db psql -U postgres -d "$POSTGRES_DB" < scripts/schema.sql
|
||||
# # then each numbered scripts/0NN_*.sql, then: docker compose restart rest
|
||||
# =============================================================================
|
||||
|
||||
name: hackerspace
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Postgres - the one durable service. Everything else is stateless.
|
||||
# supabase/postgres ships the roles (anon, authenticated, service_role,
|
||||
# authenticator, supabase_admin, supabase_auth_admin, ...) and the logical
|
||||
# replication config that GoTrue / PostgREST / Realtime expect on first boot.
|
||||
# ---------------------------------------------------------------------------
|
||||
db:
|
||||
image: supabase/postgres:15.8.1.060
|
||||
container_name: hackerspace_db
|
||||
restart: unless-stopped
|
||||
# The image must start with its own config file; a bare `postgres` command
|
||||
# skips it and the first-boot init never creates the `postgres` role.
|
||||
command:
|
||||
- postgres
|
||||
- -c
|
||||
- config_file=/etc/postgresql/postgresql.conf
|
||||
- -c
|
||||
- log_min_messages=fatal
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
POSTGRES_PORT: "5432"
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
JWT_EXP: ${JWT_EXPIRY}
|
||||
ports:
|
||||
# Admin port: bound to localhost only (psql / pg_dump from the host).
|
||||
- "127.0.0.1:${POSTGRES_HOST_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
# First-boot init SQL (runs only on an empty volume): service-role
|
||||
# passwords, JWT settings, and the _realtime schema.
|
||||
- ./supabase/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro
|
||||
- ./supabase/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro
|
||||
- ./supabase/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres -h localhost -d ${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
start_period: 30s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GoTrue (auth) - /auth/v1/* behind Kong.
|
||||
# ---------------------------------------------------------------------------
|
||||
auth:
|
||||
image: supabase/gotrue:v2.177.0
|
||||
container_name: hackerspace_auth
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
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}@db:5432/${POSTGRES_DB}
|
||||
|
||||
GOTRUE_SITE_URL: ${SITE_URL}
|
||||
GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS}
|
||||
GOTRUE_DISABLE_SIGNUP: "false"
|
||||
|
||||
GOTRUE_JWT_ADMIN_ROLES: service_role
|
||||
GOTRUE_JWT_AUD: authenticated
|
||||
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
|
||||
GOTRUE_JWT_EXP: ${JWT_EXPIRY}
|
||||
GOTRUE_JWT_SECRET: ${JWT_SECRET}
|
||||
|
||||
GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP}
|
||||
GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM}
|
||||
GOTRUE_EXTERNAL_PHONE_ENABLED: "false"
|
||||
GOTRUE_SMS_AUTOCONFIRM: "false"
|
||||
|
||||
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: /auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
|
||||
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:9999/health",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PostgREST (rest) - /rest/v1/* behind Kong.
|
||||
# The postgrest image is a distroless Haskell binary with no shell/curl, so
|
||||
# it cannot self-healthcheck. Readiness for the app is therefore gated on
|
||||
# Kong (which fronts rest); see the app service's depends_on below.
|
||||
# ---------------------------------------------------------------------------
|
||||
rest:
|
||||
image: postgrest/postgrest:v12.2.12
|
||||
container_name: hackerspace_rest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS}
|
||||
PGRST_DB_ANON_ROLE: anon
|
||||
PGRST_JWT_SECRET: ${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 - /realtime/v1/* behind Kong. The container name is significant:
|
||||
# Kong routes to realtime-dev.supabase-realtime, matching upstream Supabase.
|
||||
# ---------------------------------------------------------------------------
|
||||
realtime:
|
||||
image: supabase/realtime:v2.34.47
|
||||
container_name: realtime-dev.supabase-realtime
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PORT: "4000"
|
||||
DB_HOST: db
|
||||
DB_PORT: "5432"
|
||||
DB_USER: supabase_admin
|
||||
DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DB_NAME: ${POSTGRES_DB}
|
||||
DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime"
|
||||
DB_ENC_KEY: supabaserealtime
|
||||
API_JWT_SECRET: ${JWT_SECRET}
|
||||
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
|
||||
ERL_AFLAGS: -proto_dist inet_tcp
|
||||
DNS_NODES: "''"
|
||||
RLIMIT_NOFILE: "10000"
|
||||
APP_NAME: realtime
|
||||
SEED_SELF_HOST: "true"
|
||||
RUN_JANITOR: "true"
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"curl",
|
||||
"-sSfL",
|
||||
"--head",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
"-H",
|
||||
"Authorization: Bearer ${ANON_KEY}",
|
||||
"http://localhost:4000/api/tenants/realtime-dev/health",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# postgres-meta - backs Studio's table editor. Not fronted by Kong here.
|
||||
# ---------------------------------------------------------------------------
|
||||
meta:
|
||||
image: supabase/postgres-meta:v0.91.0
|
||||
container_name: hackerspace_meta
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
PG_META_PORT: "8080"
|
||||
PG_META_DB_HOST: db
|
||||
PG_META_DB_PORT: "5432"
|
||||
PG_META_DB_NAME: ${POSTGRES_DB}
|
||||
PG_META_DB_USER: supabase_admin
|
||||
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kong - the API gateway. Everything the app / browser talks to goes here:
|
||||
# /auth/v1/* -> auth:9999
|
||||
# /rest/v1/* -> rest:3000
|
||||
# /realtime/v1/* -> realtime:4000
|
||||
# Declarative (DB-less) config is generated inline from the ANON_KEY /
|
||||
# SERVICE_ROLE_KEY env vars so this bundle needs no vendored kong.yml.
|
||||
# NOTE: $$VAR below is a literal `$VAR` after compose interpolation; the
|
||||
# heredoc runs in the container shell, which expands it from Kong's env.
|
||||
# ---------------------------------------------------------------------------
|
||||
kong:
|
||||
image: kong:2.8.1
|
||||
container_name: hackerspace_kong
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
auth:
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
realtime:
|
||||
condition: service_started
|
||||
ports:
|
||||
# API gateway. Bound to localhost: a reverse proxy (or the browser on the
|
||||
# same host) reaches it here. Do NOT expose 0.0.0.0 on the public internet
|
||||
# without TLS in front - see docs/DEPLOY_DO_SELFHOSTED.md.
|
||||
- "127.0.0.1:${KONG_HTTP_PORT}:8000"
|
||||
- "127.0.0.1:${KONG_HTTPS_PORT}:8443"
|
||||
environment:
|
||||
KONG_DATABASE: "off"
|
||||
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
|
||||
KONG_DNS_ORDER: LAST,A,CNAME
|
||||
KONG_PLUGINS: request-transformer,cors,key-auth,acl
|
||||
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
|
||||
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
entrypoint:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
cat > /home/kong/kong.yml <<EOF
|
||||
_format_version: '2.1'
|
||||
_transform: true
|
||||
|
||||
consumers:
|
||||
- username: anon
|
||||
keyauth_credentials:
|
||||
- key: $$SUPABASE_ANON_KEY
|
||||
- username: service_role
|
||||
keyauth_credentials:
|
||||
- key: $$SUPABASE_SERVICE_KEY
|
||||
|
||||
acls:
|
||||
- consumer: anon
|
||||
group: anon
|
||||
- consumer: service_role
|
||||
group: admin
|
||||
|
||||
services:
|
||||
- name: auth-v1-open
|
||||
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
|
||||
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
|
||||
url: http://auth:9999/authorize
|
||||
routes:
|
||||
- name: auth-v1-open-authorize
|
||||
strip_path: true
|
||||
paths:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: auth-v1
|
||||
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: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
- name: rest-v1
|
||||
url: http://rest:3000/
|
||||
routes:
|
||||
- name: rest-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
- name: realtime-v1
|
||||
url: http://realtime-dev.supabase-realtime:4000/socket/
|
||||
routes:
|
||||
- name: realtime-v1-all
|
||||
strip_path: true
|
||||
paths:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
- name: acl
|
||||
config:
|
||||
hide_groups_header: true
|
||||
allow:
|
||||
- admin
|
||||
- anon
|
||||
EOF
|
||||
exec /docker-entrypoint.sh kong docker-start
|
||||
healthcheck:
|
||||
test: ["CMD", "kong", "health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 20s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Studio - the admin dashboard. Bound to localhost only; put it behind a
|
||||
# reverse proxy with auth before exposing it (see the DEPLOY doc, section 11).
|
||||
# ---------------------------------------------------------------------------
|
||||
studio:
|
||||
image: supabase/studio:2025.06.30-sha-6f5982d
|
||||
container_name: hackerspace_studio
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
kong:
|
||||
condition: service_healthy
|
||||
meta:
|
||||
condition: service_started
|
||||
ports:
|
||||
# Admin dashboard: localhost only.
|
||||
- "127.0.0.1:${STUDIO_PORT}:3000"
|
||||
environment:
|
||||
STUDIO_PG_META_URL: http://meta:8080
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION}
|
||||
DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT}
|
||||
SUPABASE_URL: http://kong:8000
|
||||
SUPABASE_PUBLIC_URL: ${SUPABASE_PUBLIC_URL}
|
||||
SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
|
||||
AUTH_JWT_SECRET: ${JWT_SECRET}
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"fetch('http://studio:3000/api/platform/profile').then((r)=>{if(r.status!==200)throw new Error(r.status)}).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The app - built from the existing Dockerfile. NEXT_PUBLIC_* values are
|
||||
# inlined at build time (Next.js), so they are passed as build args AND at
|
||||
# runtime. Point NEXT_PUBLIC_SUPABASE_URL at the browser-reachable Kong URL
|
||||
# (http://localhost:8000 for a local bundle).
|
||||
# ---------------------------------------------------------------------------
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
|
||||
NEXT_PUBLIC_SINGLE_TENANT: ${NEXT_PUBLIC_SINGLE_TENANT:-}
|
||||
NEXT_PUBLIC_SITE_NAME: ${NEXT_PUBLIC_SITE_NAME:-}
|
||||
NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG: ${NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG:-}
|
||||
NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN: ${NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN:-}
|
||||
NEXT_PUBLIC_SHOW_MARKETING: ${NEXT_PUBLIC_SHOW_MARKETING:-}
|
||||
image: hackerspace-management:latest
|
||||
container_name: hackerspace_app
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
# App waits until the API gateway is healthy. Kong fronts rest/auth/
|
||||
# realtime, so a healthy Kong means the data plane is reachable. rest has
|
||||
# no in-image shell to self-healthcheck, so it is gated here as started
|
||||
# and effectively covered by Kong's own upstream readiness.
|
||||
kong:
|
||||
condition: service_healthy
|
||||
auth:
|
||||
condition: service_healthy
|
||||
rest:
|
||||
condition: service_started
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file:
|
||||
- .env.local
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
|
||||
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL}
|
||||
# Single-tenant knobs (optional; see .env.example / docs/SINGLE_TENANT.md)
|
||||
# (also passed as build args above: Next.js inlines NEXT_PUBLIC_* at build time)
|
||||
NEXT_PUBLIC_SINGLE_TENANT: ${NEXT_PUBLIC_SINGLE_TENANT}
|
||||
NEXT_PUBLIC_SITE_NAME: ${NEXT_PUBLIC_SITE_NAME}
|
||||
NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG: ${NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG}
|
||||
NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN: ${NEXT_PUBLIC_SINGLE_TENANT_OPEN_JOIN}
|
||||
NEXT_PUBLIC_SHOW_MARKETING: ${NEXT_PUBLIC_SHOW_MARKETING}
|
||||
SECRETS_ENCRYPTION_KEY: ${SECRETS_ENCRYPTION_KEY}
|
||||
CRON_SECRET: ${CRON_SECRET}
|
||||
RESEND_API_KEY: ${RESEND_API_KEY:-}
|
||||
EMAIL_FROM: ${EMAIL_FROM:-}
|
||||
SENTRY_DSN: ${SENTRY_DSN:-}
|
||||
NEXT_PUBLIC_OAUTH_GITHUB: ${NEXT_PUBLIC_OAUTH_GITHUB:-}
|
||||
NEXT_PUBLIC_OAUTH_GOOGLE: ${NEXT_PUBLIC_OAUTH_GOOGLE:-}
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
@@ -36,6 +471,10 @@ services:
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
# Postgres data survives `docker compose down`. Use `down -v` to wipe it.
|
||||
db-data:
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: hackerspace_net
|
||||
|
||||
@@ -459,17 +459,17 @@ chmod 600 /opt/hackerspace/.env.local
|
||||
|
||||
### 10.3 Build and start the app
|
||||
|
||||
The repo ships with a Dockerfile that produces a standalone Next.js image, and a docker-compose.yml that runs it. The Dockerfile expects build-time access to the public Supabase URL and anon key (Next.js inlines `NEXT_PUBLIC_*` at build time), so pass them as build args.
|
||||
The repo ships with a Dockerfile that produces a standalone Next.js image, and a `docker-compose.app.yml` that runs it (app only; `docker-compose.yml` is the all-in-one local stack and is not used here). The Dockerfile expects build-time access to the public Supabase URL and anon key (Next.js inlines `NEXT_PUBLIC_*` at build time), so pass them as build args.
|
||||
|
||||
```bash
|
||||
cd /opt/hackerspace
|
||||
docker compose build \
|
||||
docker compose -f docker-compose.app.yml build \
|
||||
--build-arg NEXT_PUBLIC_SUPABASE_URL=https://supabase.yourdomain.com \
|
||||
--build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="<ANON_KEY>" \
|
||||
--build-arg NEXT_PUBLIC_APP_URL=https://yourdomain.com
|
||||
|
||||
docker compose up -d
|
||||
docker compose logs -f app
|
||||
docker compose -f docker-compose.app.yml up -d
|
||||
docker compose -f docker-compose.app.yml logs -f app
|
||||
```
|
||||
|
||||
Wait for `Ready in <ms>`. Then verify locally on the Droplet:
|
||||
@@ -725,11 +725,11 @@ Test restores at least quarterly on a staging Droplet. A backup you have not res
|
||||
```bash
|
||||
cd /opt/hackerspace
|
||||
git pull origin main
|
||||
docker compose build \
|
||||
docker compose -f docker-compose.app.yml build \
|
||||
--build-arg NEXT_PUBLIC_SUPABASE_URL=https://supabase.yourdomain.com \
|
||||
--build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="<ANON_KEY>" \
|
||||
--build-arg NEXT_PUBLIC_APP_URL=https://yourdomain.com
|
||||
docker compose up -d --no-deps app
|
||||
docker compose -f docker-compose.app.yml up -d --no-deps app
|
||||
docker image prune -f
|
||||
```
|
||||
|
||||
@@ -762,7 +762,7 @@ Read the [release notes](https://github.com/supabase/supabase/releases) before p
|
||||
|
||||
```bash
|
||||
# App
|
||||
docker compose -f /opt/hackerspace/docker-compose.yml logs -f app
|
||||
docker compose -f /opt/hackerspace/docker-compose.app.yml logs -f app
|
||||
|
||||
# Supabase
|
||||
cd /opt/supabase/docker
|
||||
@@ -823,7 +823,7 @@ Before you point real users at this:
|
||||
|
||||
Almost always wrong env vars. Confirm `/opt/hackerspace/.env.local` has the four required values and that `NEXT_PUBLIC_SUPABASE_URL` exactly matches the URL you used in `API_EXTERNAL_URL` in the Supabase `.env`.
|
||||
|
||||
`docker compose -f /opt/hackerspace/docker-compose.yml logs app | tail -50` will surface the actual error.
|
||||
`docker compose -f /opt/hackerspace/docker-compose.app.yml logs app | tail -50` will surface the actual error.
|
||||
|
||||
### "Invalid API key" from every Supabase call
|
||||
|
||||
|
||||
@@ -4,6 +4,24 @@ Append-only. Newest entries on top. Keep each entry to one screen.
|
||||
|
||||
---
|
||||
|
||||
## 2026-10-11 (docker-compose = full local stack + secret generator)
|
||||
|
||||
Branch `main`, uncommitted.
|
||||
|
||||
- `docker-compose.yml` is now the all-in-one local stack (app + Postgres, GoTrue, PostgREST, Realtime, Kong, Studio). It is the former `docker-compose.full.yml`; compose project and network renamed to `hackerspace` / `hackerspace_net`.
|
||||
- The old app-only file is `docker-compose.app.yml`. `docs/DEPLOY_DO_SELFHOSTED.md` commands updated to pass `-f docker-compose.app.yml`.
|
||||
- `.env.example` replaced by the former `.env.full.example` plus app secrets (`SECRETS_ENCRYPTION_KEY`, `CRON_SECRET`, email, Sentry, OAuth, `SETUP_*`). `.env.full.example` removed.
|
||||
- New `scripts/generate-env.sh`: copies `.env.example` to `.env`, fills POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY (HS256 JWTs), SECRET_KEY_BASE, SECRETS_ENCRYPTION_KEY, CRON_SECRET. Asks for confirmation (default N; `--yes` skips). `.env` added to `.gitignore`.
|
||||
- `deploy/deploy.sh` now passes `-f docker-compose.app.yml` so the CD path keeps deploying the app only.
|
||||
- Host port for Postgres is now `POSTGRES_HOST_PORT` (was `POSTGRES_PORT`, which the db image also reads as its internal port; setting it to 5433 broke first-boot init). Existing `.env` files need the rename.
|
||||
- `supabase/db/*.sql` + db `command` fix: the db container must start with the image's own `config_file` and mount the first-boot init SQL, or the `postgres` role is never created (found on first real deploy). Verified on a standalone db container only (roles, passwords, `_realtime`).
|
||||
- Booted locally (arm64): all services healthy after fixing the realtime healthcheck (needs the anon key header) and the studio healthcheck (listens on its hostname, not localhost). `scripts/schema.sql` alone creates all 52 tables; it records only migrations 014-017 as applied, so `apply-migrations.sh` on a fresh DB fails at 018 (view column rename). Fresh installs: run schema.sql only. Not fixed.
|
||||
- Single-tenant flags (`NEXT_PUBLIC_SINGLE_TENANT`, `_SITE_NAME`, `_SINGLE_TENANT_SPACE_SLUG`, `_SINGLE_TENANT_OPEN_JOIN`, `_SHOW_MARKETING`) are now Dockerfile build args and compose build args. Before this, the Docker image ignored them (Next inlines `NEXT_PUBLIC_*` at build), so the createSpace guard was never active in containers. Not yet verified with a rebuild.
|
||||
- New `scripts/setup-space.sh`: runs `scripts/setup.mjs` (same as `pnpm setup`) in a container built from the Dockerfile `deps` stage on `hackerspace_net`, reading `.env`. Host needs only Docker. Applies `scripts/schema.sql` itself when `public.spaces` is missing (fresh volume). Verified `doctor` after `down -v`; provision and create-admin not run.
|
||||
- Decision for you: the script writes `.env`, not `.env.example`, since `.env.example` is committed. Use `--output .env.example` only if you accept real secrets in a tracked file.
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-05 (single-tenant mode + setup CLI + security fix)
|
||||
|
||||
Shipped **single-tenant deployment mode** so a hackerspace can run its own private instance (own DB, own
|
||||
|
||||
@@ -103,7 +103,8 @@ Single-tenant instances deploy exactly like any instance. The moving parts:
|
||||
|
||||
- `deploy/deploy.sh` runs the standard release: fetch, install, apply pending migrations, build, restart. It is idempotent, so a redeploy with no new migrations does not touch the database.
|
||||
- `scripts/apply-migrations.sh` applies any `scripts/0NN_*.sql` not yet recorded in `public._migrations_applied` and records each filename. It does not restart PostgREST; do that separately afterward so PostgREST refreshes its schema cache (`deploy/deploy.sh` does this automatically, or run `docker compose restart rest` when applying migrations by hand).
|
||||
- `docker-compose.full.yml` brings up the full stack (Supabase plus the app) for a container-based host.
|
||||
- `scripts/setup-space.sh` creates the space and first admin on the `docker-compose.yml` stack without Node on the host. It takes the same flags as `pnpm setup` (`doctor`, `--yes`, `--admin-email`, and so on) and defaults the space name and slug from `NEXT_PUBLIC_SITE_NAME` and `NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG` in `.env`.
|
||||
- `docker-compose.yml` brings up the full stack (Supabase plus the app) for a container-based host. Run `scripts/generate-env.sh` first to create `.env` with fresh secrets (the app-only file is `docker-compose.app.yml`).
|
||||
|
||||
Set every `NEXT_PUBLIC_` value on the host before building, because Next.js inlines them at build time. Run `pnpm setup` once after the first successful deploy (it is a no-op on later deploys). Full procedure: [DEPLOY_DO_SELFHOSTED.md](./DEPLOY_DO_SELFHOSTED.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# pnpm 11+ fails `pnpm install --frozen-lockfile` on dependency build scripts
|
||||
# that have not been reviewed. sharp (next/image) and esbuild/unrs-resolver
|
||||
# (tooling) ship native binaries via postinstall. msw's script only copies a
|
||||
# browser worker and is not needed here.
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: true
|
||||
unrs-resolver: true
|
||||
msw: false
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# generate-env.sh: copy .env.example to .env and fill in every secret.
|
||||
#
|
||||
# Fills POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY,
|
||||
# SECRET_KEY_BASE, SECRETS_ENCRYPTION_KEY and CRON_SECRET. ANON_KEY and
|
||||
# SERVICE_ROLE_KEY are HS256 JWTs signed with the generated JWT_SECRET.
|
||||
#
|
||||
# Usage: scripts/generate-env.sh [--output FILE] [--yes]
|
||||
# --output FILE write here instead of .env
|
||||
# --yes, --force skip the confirmation prompt (required when stdin is not a
|
||||
# terminal)
|
||||
#
|
||||
# Always asks before writing; the default answer is N.
|
||||
#
|
||||
# The default target is .env (git-ignored), not .env.example, because
|
||||
# .env.example is committed and must never hold real secrets.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TEMPLATE="$ROOT/.env.example"
|
||||
OUT="$ROOT/.env"
|
||||
FORCE=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output) OUT="$2"; shift 2 ;;
|
||||
--yes|--force) FORCE=1; shift ;;
|
||||
-h|--help) sed -n '2,18p' "${BASH_SOURCE[0]}"; exit 0 ;;
|
||||
*) echo "error: unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v openssl >/dev/null || { echo "error: openssl is required" >&2; exit 1; }
|
||||
[[ -f "$TEMPLATE" ]] || { echo "error: $TEMPLATE not found" >&2; exit 1; }
|
||||
if [[ $FORCE -ne 1 ]]; then
|
||||
if [[ ! -t 0 ]]; then
|
||||
echo "error: not a terminal; pass --yes to run non-interactively" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -e "$OUT" ]]; then
|
||||
echo "$OUT already exists. Continuing replaces it and rotates every secret."
|
||||
else
|
||||
echo "This will create $OUT with newly generated secrets."
|
||||
fi
|
||||
read -r -p "Continue? [y/N] " answer
|
||||
case "$answer" in
|
||||
y|Y|yes|YES) ;;
|
||||
*) echo "aborted"; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
|
||||
|
||||
# sign_jwt ROLE SECRET -> HS256 JWT valid for 10 years
|
||||
sign_jwt() {
|
||||
local role="$1" secret="$2" iat header payload sig
|
||||
iat="$(date +%s)"
|
||||
header="$(printf '{"alg":"HS256","typ":"JWT"}' | b64url)"
|
||||
payload="$(printf '{"role":"%s","iss":"supabase","iat":%d,"exp":%d}' \
|
||||
"$role" "$iat" "$((iat + 315360000))" | b64url)"
|
||||
sig="$(printf '%s.%s' "$header" "$payload" \
|
||||
| openssl dgst -sha256 -hmac "$secret" -binary | b64url)"
|
||||
printf '%s.%s.%s' "$header" "$payload" "$sig"
|
||||
}
|
||||
|
||||
POSTGRES_PASSWORD="$(openssl rand -hex 24)"
|
||||
JWT_SECRET="$(openssl rand -hex 32)"
|
||||
ANON_KEY="$(sign_jwt anon "$JWT_SECRET")"
|
||||
SERVICE_ROLE_KEY="$(sign_jwt service_role "$JWT_SECRET")"
|
||||
SECRET_KEY_BASE="$(openssl rand -hex 32)"
|
||||
SECRETS_ENCRYPTION_KEY="$(openssl rand -hex 32)"
|
||||
CRON_SECRET="$(openssl rand -hex 32)"
|
||||
|
||||
umask 077
|
||||
cp "$TEMPLATE" "$OUT"
|
||||
for name in POSTGRES_PASSWORD JWT_SECRET ANON_KEY SERVICE_ROLE_KEY \
|
||||
SECRET_KEY_BASE SECRETS_ENCRYPTION_KEY CRON_SECRET; do
|
||||
# Values are hex or base64url, so they contain no sed-special characters.
|
||||
sed -i "s|^${name}=.*|${name}=${!name}|" "$OUT"
|
||||
done
|
||||
chmod 600 "$OUT"
|
||||
|
||||
echo "wrote $OUT"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# setup-space.sh: create the single space and its first admin on the
|
||||
# docker-compose.yml stack. Wraps scripts/setup.mjs (the same code as
|
||||
# `pnpm setup`) and runs it in a container on the stack's network, so the
|
||||
# host needs only Docker: no Node, no pnpm, no node_modules.
|
||||
#
|
||||
# Usage: scripts/setup-space.sh [command] [setup.mjs flags]
|
||||
# scripts/setup-space.sh interactive: space, then admin
|
||||
# scripts/setup-space.sh doctor check config and DB connectivity
|
||||
# scripts/setup-space.sh --yes \
|
||||
# --admin-email you@example.org --admin-password '...'
|
||||
#
|
||||
# Reads from .env: SERVICE_ROLE_KEY, ANON_KEY, NEXT_PUBLIC_APP_URL,
|
||||
# NEXT_PUBLIC_SITE_NAME and NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG (used as the
|
||||
# default space name and slug), and any SETUP_* values. Flags override these.
|
||||
# The stack must already be running. If the database has no schema yet (a fresh
|
||||
# volume), scripts/schema.sql is applied first.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
ENV_FILE="$ROOT/.env"
|
||||
NETWORK="hackerspace_net"
|
||||
IMAGE="hackerspace-setup:local"
|
||||
|
||||
command -v docker >/dev/null || { echo "error: docker is required" >&2; exit 1; }
|
||||
[[ -f "$ENV_FILE" ]] || { echo "error: $ENV_FILE not found; run scripts/generate-env.sh first" >&2; exit 1; }
|
||||
docker network inspect "$NETWORK" >/dev/null 2>&1 \
|
||||
|| { echo "error: network $NETWORK not found; start the stack with 'docker compose up -d'" >&2; exit 1; }
|
||||
|
||||
# Read single keys instead of sourcing the file, so nothing in it is executed.
|
||||
env_get() { grep -E "^$1=" "$ENV_FILE" | tail -n1 | cut -d= -f2- || true; }
|
||||
|
||||
SERVICE_KEY="$(env_get SERVICE_ROLE_KEY)"
|
||||
ANON="$(env_get ANON_KEY)"
|
||||
[[ -n "$SERVICE_KEY" && -n "$ANON" ]] \
|
||||
|| { echo "error: SERVICE_ROLE_KEY or ANON_KEY is empty in .env" >&2; exit 1; }
|
||||
|
||||
export NEXT_PUBLIC_SUPABASE_URL="http://kong:8000"
|
||||
export NEXT_PUBLIC_SUPABASE_ANON_KEY="$ANON"
|
||||
export SUPABASE_SERVICE_ROLE_KEY="$SERVICE_KEY"
|
||||
export NEXT_PUBLIC_APP_URL="$(env_get NEXT_PUBLIC_APP_URL)"
|
||||
|
||||
for name in SETUP_SPACE_NAME SETUP_SPACE_SLUG SETUP_SPACE_CITY \
|
||||
SETUP_ADMIN_EMAIL SETUP_ADMIN_PASSWORD SETUP_ADMIN_NAME; do
|
||||
export "$name=$(env_get "$name")"
|
||||
done
|
||||
[[ -n "$SETUP_SPACE_NAME" ]] || export SETUP_SPACE_NAME="$(env_get NEXT_PUBLIC_SITE_NAME)"
|
||||
[[ -n "$SETUP_SPACE_SLUG" ]] || export SETUP_SPACE_SLUG="$(env_get NEXT_PUBLIC_SINGLE_TENANT_SPACE_SLUG)"
|
||||
|
||||
dc() { docker compose -f "$ROOT/docker-compose.yml" "$@"; }
|
||||
|
||||
if [[ "$(dc exec -T db psql -U postgres -d postgres -Atc "select to_regclass('public.spaces') is not null" 2>/dev/null)" != "t" ]]; then
|
||||
echo "No schema found; applying scripts/schema.sql"
|
||||
dc exec -T db psql -v ON_ERROR_STOP=1 -q -U postgres -d postgres < "$ROOT/scripts/schema.sql" >/dev/null
|
||||
# PostgREST caches the schema; restart so it sees the new tables.
|
||||
dc restart rest >/dev/null
|
||||
until [[ "$(dc ps rest --format '{{.State}}')" == "running" ]]; do sleep 1; done
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
# The Dockerfile deps stage is Node plus installed node_modules; the scripts
|
||||
# directory is mounted so edits to setup.mjs apply without a rebuild.
|
||||
docker build -q --target deps -t "$IMAGE" "$ROOT" >/dev/null
|
||||
|
||||
tty_flags=()
|
||||
[[ -t 0 && -t 1 ]] && tty_flags=(-it)
|
||||
|
||||
# `-e NAME` without a value forwards the variable, so secrets stay off the
|
||||
# docker command line.
|
||||
exec docker run --rm "${tty_flags[@]}" --network "$NETWORK" \
|
||||
-v "$ROOT/scripts:/app/scripts:ro" \
|
||||
-e NEXT_PUBLIC_SUPABASE_URL -e NEXT_PUBLIC_SUPABASE_ANON_KEY \
|
||||
-e SUPABASE_SERVICE_ROLE_KEY -e NEXT_PUBLIC_APP_URL \
|
||||
-e SETUP_SPACE_NAME -e SETUP_SPACE_SLUG -e SETUP_SPACE_CITY \
|
||||
-e SETUP_ADMIN_EMAIL -e SETUP_ADMIN_PASSWORD -e SETUP_ADMIN_NAME \
|
||||
"$IMAGE" node scripts/setup.mjs "$@"
|
||||
@@ -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,4 @@
|
||||
\set pguser `echo "$POSTGRES_USER"`
|
||||
|
||||
create schema if not exists _realtime;
|
||||
alter schema _realtime owner to :pguser;
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Sets the service-role passwords to POSTGRES_PASSWORD on first boot, so
|
||||
-- auth, rest and realtime can log in with the one password in .env.
|
||||
\set pgpass `echo "$POSTGRES_PASSWORD"`
|
||||
|
||||
ALTER USER authenticator WITH PASSWORD :'pgpass';
|
||||
ALTER USER pgbouncer WITH PASSWORD :'pgpass';
|
||||
ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass';
|
||||
Reference in New Issue
Block a user