#!/usr/bin/env bash # setup.sh — Prepare the local development environment. # Safe to run multiple times: every step is a no-op if already complete. # # What it does: # 1. Checks prerequisites (docker, pnpm); installs just if missing # 2. Adds 127.0.0.1 keycloak to /etc/hosts (needed for GoTrue ↔ Keycloak) # 3. Creates .env with dev defaults if it doesn't exist # 4. Fixes supabase/migrations permissions if root-owned (common Docker artifact) # 5. Installs pnpm dependencies # 6. Starts the Docker stack # 7. Waits for postgres, kong, and keycloak to be healthy # 8. Applies SQL migrations (tracked — won't re-apply) # 9. Seeds the database (no-op if already seeded) # 10. Compiles Paraglide i18n files set -euo pipefail # ── Colours ─────────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' step() { echo; echo -e "${BOLD}${BLUE}==> $*${NC}"; } ok() { echo -e " ${GREEN}✓${NC} $*"; } warn() { echo -e " ${YELLOW}!${NC} $*"; } die() { echo -e "\n${RED}ERROR:${NC} $*" >&2; exit 1; } # ── Resolve project root ────────────────────────────────────────────────────── ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT" DC="docker compose --env-file .env -f infra/docker-compose.dev.yml" DB_CONTAINER="colectivo-dev-db-1" # ── 1. Prerequisites ────────────────────────────────────────────────────────── step "Checking prerequisites" command -v docker >/dev/null 2>&1 \ || die "docker not found. Install from https://docs.docker.com/get-docker/" docker compose version >/dev/null 2>&1 \ || die "docker compose v2 not found. Update Docker or install the compose plugin." ok "docker $(docker --version | grep -oP '[\d.]+' | head -1)" command -v pnpm >/dev/null 2>&1 \ || die "pnpm not found. Install: curl -fsSL https://get.pnpm.io/install.sh | sh" ok "pnpm $(pnpm --version)" command -v python3 >/dev/null 2>&1 \ || die "python3 not found — required to generate JWT tokens." if ! command -v just >/dev/null 2>&1; then warn "just not found — installing to /usr/local/bin (requires sudo)" curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \ | sudo bash -s -- --to /usr/local/bin ok "just $(just --version)" else ok "just $(just --version)" fi # ── 2. /etc/hosts ───────────────────────────────────────────────────────────── step "Checking /etc/hosts for 'keycloak' → 127.0.0.1" if grep -qE '^\s*127\.0\.0\.1\s+.*\bkeycloak\b' /etc/hosts 2>/dev/null; then ok "Entry already present" else warn "Adding '127.0.0.1 keycloak' to /etc/hosts (requires sudo)" echo "127.0.0.1 keycloak" | sudo tee -a /etc/hosts >/dev/null ok "Entry added" fi # ── 3. .env ──────────────────────────────────────────────────────────────────── step "Setting up .env" if [[ -f .env ]]; then ok ".env already exists — skipping" else warn ".env not found — generating with dev defaults" # Well-known dev-only JWT secret. Never use in production. JWT_SECRET="super-secret-jwt-token-with-at-least-32-characters-long" # Generate anon and service_role JWTs signed with the dev secret. # Python 3 is available on virtually every Linux/macOS dev machine. IFS=' ' read -r ANON_KEY SERVICE_KEY < <(python3 - < .env < [timeout_seconds] wait_for() { local label="$1" local cmd="$2" local max="${3:-90}" local n=0 printf " %-18s" "$label" until eval "$cmd" >/dev/null 2>&1; do n=$((n + 2)) if (( n >= max )); then echo -e " ${RED}timed out${NC}" die "$label did not become healthy after ${max}s. Check: docker compose -f infra/docker-compose.dev.yml logs" fi sleep 2 printf "." done echo -e " ${GREEN}ready${NC}" } # PostgreSQL must be first — everything else depends on it. wait_for "postgres" "docker exec ${DB_CONTAINER} pg_isready -U postgres -q" # GoTrue (auth) is reached through Kong. wait_for "kong/auth" "curl -sf http://localhost:8001/auth/v1/health" # Keycloak realm endpoint — ready only after realm import completes. wait_for "keycloak" "curl -sf http://localhost:8080/realms/colectivo" 120 # ── 8. Apply database migrations ────────────────────────────────────────────── step "Applying database migrations" # Create migration tracking table in the public schema (idempotent). docker exec "$DB_CONTAINER" psql -U postgres -d postgres -q \ -c "CREATE TABLE IF NOT EXISTS public._applied_migrations ( filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ DEFAULT now() );" \ 2>/dev/null || die "Could not create migration tracking table" shopt -s nullglob migration_files=(supabase/migrations/*.sql) shopt -u nullglob if (( ${#migration_files[@]} == 0 )); then warn "No migration files found in supabase/migrations/" else for sql_file in "${migration_files[@]}"; do filename="$(basename "$sql_file")" already_applied=$(docker exec "$DB_CONTAINER" psql -U postgres -d postgres -tAq \ -c "SELECT count(*) FROM public._applied_migrations WHERE filename = '${filename}'" \ 2>/dev/null) if [[ "${already_applied:-0}" -gt 0 ]]; then ok "$filename (already applied)" continue fi printf " Applying %-40s" "$filename ..." if docker exec -i "$DB_CONTAINER" \ psql -U postgres -d postgres -q -v ON_ERROR_STOP=1 \ < "$sql_file" 2>/tmp/migration_err; then docker exec "$DB_CONTAINER" psql -U postgres -d postgres -q \ -c "INSERT INTO public._applied_migrations (filename) VALUES ('${filename}') ON CONFLICT DO NOTHING;" echo -e " ${GREEN}done${NC}" else echo -e " ${RED}FAILED${NC}" echo "--- error output ---" cat /tmp/migration_err die "Migration ${filename} failed. Fix the error and re-run setup.sh." fi done fi # ── 9. Seed database ─────────────────────────────────────────────────────────── step "Seeding database" user_count=$(docker exec "$DB_CONTAINER" psql -U postgres -d postgres -tAq \ -c "SELECT count(*) FROM public.users" 2>/dev/null || echo "0") if (( user_count > 0 )); then ok "Already seeded ($user_count users in public.users)" else printf " Applying seed.sql ..." if docker exec -i "$DB_CONTAINER" \ psql -U postgres -d postgres -q -v ON_ERROR_STOP=1 \ < supabase/seed.sql 2>/tmp/seed_err; then echo -e " ${GREEN}done${NC}" else echo -e " ${RED}FAILED${NC}" cat /tmp/seed_err die "seed.sql failed. Fix the error and re-run setup.sh." fi fi # ── 10. Compile Paraglide i18n ────────────────────────────────────────────── step "Compiling Paraglide i18n" PARAGLIDE_OUTDIR="apps/web/src/lib/paraglide" if [[ -d "$PARAGLIDE_OUTDIR" && -f "$PARAGLIDE_OUTDIR/runtime.js" ]]; then ok "Already compiled" else PARAGLIDE_BIN="$(find node_modules/.pnpm -name 'paraglide-js' -path '*/.bin/paraglide-js' \ 2>/dev/null | head -1)" if [[ -z "$PARAGLIDE_BIN" ]]; then warn "paraglide-js binary not found — skipping (run 'pnpm install' and re-run setup.sh)" else "$PARAGLIDE_BIN" compile \ --project apps/web/project.inlang \ --outdir "$PARAGLIDE_OUTDIR" ok "Compiled to $PARAGLIDE_OUTDIR" fi fi # ── Done ─────────────────────────────────────────────────────────────────────── echo echo -e "${BOLD}${GREEN}✓ Local environment is ready.${NC}" echo echo " Start the app: just dev" echo " Supabase Studio: http://localhost:54323" echo " Keycloak Admin: http://localhost:8080/admin (admin / admin)" echo " Test users (pw: test1234): ana, borja, carmen, david, eva" echo