Updated
CI / check (push) Canceled after 0s
CI / integration-e2e (push) Canceled after 0s
Deploy to production / deploy (push) Canceled after 0s

This commit is contained in:
2026-10-14 23:30:02 +02:00
parent 04145e5985
commit d81a49175b
18 changed files with 867 additions and 716 deletions
+144 -96
View File
@@ -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.
# -----------------------------------------------------------------------------
-137
View File
@@ -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=
+1
View File
@@ -6,6 +6,7 @@ __v0_jsx-dev-runtime.ts
# Common ignores
node_modules/
.next/
.env
.env*.local
.DS_Store
+11 -1
View File
@@ -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
View File
@@ -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
+46
View File
@@ -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
-464
View File
@@ -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
View File
@@ -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
+8 -8
View File
@@ -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
+18
View File
@@ -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
+2 -1
View File
@@ -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).
+9
View File
@@ -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
+85
View File
@@ -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"
+78
View File
@@ -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 "$@"
+1
View File
@@ -0,0 +1 @@
pi --session 01a12bd3-8dff-74eb-97b8-a99e9f987612
+5
View File
@@ -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';
+4
View File
@@ -0,0 +1,4 @@
\set pguser `echo "$POSTGRES_USER"`
create schema if not exists _realtime;
alter schema _realtime owner to :pguser;
+7
View File
@@ -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';