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.
104 lines
3.8 KiB
Bash
Executable File
104 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# apply-migrations.sh — idempotently apply pending numbered SQL migrations.
|
|
#
|
|
# For every scripts/0NN_*.sql (in sorted order) this checks public._migrations_applied
|
|
# for that filename and, if it is absent, applies the file and records the filename.
|
|
# Each file is applied inside a single transaction together with its tracking INSERT,
|
|
# so a failure rolls the whole thing back and the file is NOT recorded as applied.
|
|
# This makes the script safe to run repeatedly: already-applied files are skipped and
|
|
# a re-run with no pending files is a no-op.
|
|
#
|
|
# How psql is invoked is parameterized via the environment:
|
|
# - DATABASE_URL set -> psql "$DATABASE_URL"
|
|
# - SUPABASE_DB_CONTAINER set -> docker compose exec -T <container> psql -U postgres -d postgres
|
|
# (optionally scoped to a compose file via SUPABASE_DIR/docker-compose.yml)
|
|
#
|
|
# Exactly one of those must be set. DATABASE_URL wins if both are present.
|
|
|
|
set -euo pipefail
|
|
|
|
# Resolve the directory this script lives in; the migrations sit alongside it.
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Decide how to reach psql.
|
|
# ---------------------------------------------------------------------------
|
|
if [[ -n "${DATABASE_URL:-}" ]]; then
|
|
PSQL_MODE="url"
|
|
elif [[ -n "${SUPABASE_DB_CONTAINER:-}" ]]; then
|
|
PSQL_MODE="docker"
|
|
else
|
|
echo "error: set DATABASE_URL or SUPABASE_DB_CONTAINER to tell this script how to reach Postgres" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Optional compose-file scope for the docker path (e.g. /opt/supabase/docker).
|
|
_compose_file_args=()
|
|
if [[ "$PSQL_MODE" == "docker" && -n "${SUPABASE_DIR:-}" ]]; then
|
|
_compose_file_args=(-f "$SUPABASE_DIR/docker-compose.yml")
|
|
fi
|
|
|
|
# run_psql: pipe SQL on stdin into Postgres. Extra args are passed to psql.
|
|
run_psql() {
|
|
if [[ "$PSQL_MODE" == "url" ]]; then
|
|
psql "$DATABASE_URL" "$@"
|
|
else
|
|
docker compose "${_compose_file_args[@]}" exec -T "$SUPABASE_DB_CONTAINER" \
|
|
psql -U postgres -d postgres "$@"
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Make sure the tracking table exists. It is also created by scripts/schema.sql
|
|
# on a fresh database; this guard keeps the script self-sufficient and idempotent.
|
|
# ---------------------------------------------------------------------------
|
|
run_psql -v ON_ERROR_STOP=1 -q <<'SQL'
|
|
CREATE TABLE IF NOT EXISTS public._migrations_applied (
|
|
filename text PRIMARY KEY,
|
|
applied_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
SQL
|
|
|
|
# already_applied FILENAME -> success (0) if the filename is recorded.
|
|
already_applied() {
|
|
local fn="$1"
|
|
local out
|
|
out="$(run_psql -tAqc "SELECT 1 FROM public._migrations_applied WHERE filename = '${fn}' LIMIT 1")"
|
|
[[ "$out" == *1* ]]
|
|
}
|
|
|
|
# apply_one FILE FILENAME -> apply the SQL and record it, all in one transaction.
|
|
# --single-transaction wraps stdin in BEGIN/COMMIT; ON_ERROR_STOP aborts (and thus
|
|
# rolls back) on the first error, so the tracking INSERT never lands for a file that
|
|
# did not fully apply.
|
|
apply_one() {
|
|
local file="$1" fn="$2"
|
|
{
|
|
cat "$file"
|
|
printf "\nINSERT INTO public._migrations_applied (filename) VALUES ('%s') ON CONFLICT (filename) DO NOTHING;\n" "$fn"
|
|
} | run_psql --single-transaction -v ON_ERROR_STOP=1 -q
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Walk the numbered migrations in sorted order.
|
|
# ---------------------------------------------------------------------------
|
|
shopt -s nullglob
|
|
mapfile -t migrations < <(printf '%s\n' "$SCRIPT_DIR"/0*.sql | sort)
|
|
shopt -u nullglob
|
|
|
|
applied_count=0
|
|
for file in "${migrations[@]}"; do
|
|
fn="$(basename "$file")"
|
|
if already_applied "$fn"; then
|
|
continue
|
|
fi
|
|
apply_one "$file" "$fn"
|
|
echo "applied $fn"
|
|
applied_count=$((applied_count + 1))
|
|
done
|
|
|
|
if [[ "$applied_count" -eq 0 ]]; then
|
|
echo "up to date"
|
|
fi
|