Files
hackerspace.sh/.env.full.example
T
Moheeb Zara 7b8d2ae167 feat(tenant): setup CLI, CD glue, bundled compose, docs + security fix
Tier 2 productization for single-tenant instances, plus fixes from an
adversarial review pass.

New:
- scripts/setup.mjs: 'pnpm setup' CLI (doctor/provision/create-admin/all),
  idempotent, creates the first admin via the Supabase Admin API (email_confirm),
  flag > env > .env.local precedence, masked interactive prompts, --yes/--force.
- scripts/apply-migrations.sh + deploy/deploy.sh: reproducible, idempotent CD
  glue (previously only on the prod droplet), tracked via public._migrations_applied.
- docker-compose.full.yml + .env.full.example: one-command app + self-hosted
  Supabase bundle (compose config validated).
- docs/SINGLE_TENANT.md (deploy + CLI guide) and docs/CUSTOMIZE.md (the
  documented lib/tenant.ts config pattern for white-labeling); README pointer.
- __tests__/tenant.test.ts: 19 unit tests for resolveTenantConfig + isMarketingPath.

Security (found by review, pre-existing):
- Drop the stale handle_space_signup() auth trigger, which derived member role
  and space from client-controlled raw_user_meta_data and could self-insert an
  admin. Membership is created only by the vetted server actions.
  Migration scripts/056_drop_stale_signup_trigger.sql + folded into schema.sql.

Fixes:
- setup.mjs masked-password prompt hung (callback passed to readline/promises
  question); use the awaited form.
- notifications-enqueue.test: base-URL default is now localhost (platform-domain
  fallback removed), not hackerspace.sh.
- docs accuracy (logo/BrandMark, apply-migrations does not restart PostgREST, --yes).

Gates: tsc 0, lint 0 errors, 664 unit tests, build ok.
2026-08-05 13:02:18 -07:00

138 lines
7.2 KiB
Bash

# =============================================================================
# .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=[email protected]
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=