diff --git a/.env.example b/.env.example index 32f78218..a51ddf59 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,13 @@ NODE_ENV=development PORT=9999 LOG_LEVEL=debug -DATABASE_URL=postgres://postgres:123456@localhost:5432/fluent +# Role passwords below are dev-only defaults — override all three in any shared or deployed environment. +# Runtime (least-privilege) role used by the app: +DATABASE_URL=postgres://api_user:password@localhost:5432/fluent +# Migration role (DDL) used by drizzle-kit: +MIGRATIONS_DATABASE_URL=postgres://api_migrator:password@localhost:5432/fluent +# Superuser used only by the bootstrap script: +BOOTSTRAP_DATABASE_URL=postgres://postgres:postgres@localhost:5432/fluent BETTER_AUTH_SECRET=generate-a-32-character-secret-key BETTER_AUTH_URL=http://localhost:9999/api/auth BETTER_AUTH_COOKIE_DOMAIN=.fluent.bible diff --git a/compose.yaml b/compose.yaml index fc0d00c9..ea373170 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,7 +9,6 @@ services: - '${DB_PORT:-5432}:5432' volumes: - fluent-pgdata:/var/lib/postgresql/data - - ./db/init:/docker-entrypoint-initdb.d:ro healthcheck: test: [CMD-SHELL, pg_isready -U postgres -d fluent] interval: 5s @@ -30,7 +29,9 @@ services: - fluent-api-node-modules:/app/node_modules env_file: .env environment: - DATABASE_URL: postgres://postgres:postgres@db:5432/fluent + BOOTSTRAP_DATABASE_URL: postgres://postgres:postgres@db:5432/fluent + MIGRATIONS_DATABASE_URL: postgres://api_migrator:password@db:5432/fluent + DATABASE_URL: postgres://api_user:password@db:5432/fluent EXPORTS_DIR: /app/exports user: '1001:1001' read_only: true @@ -62,7 +63,9 @@ services: - fluent-worker-node-modules:/app/node_modules env_file: .env environment: - DATABASE_URL: postgres://postgres:postgres@db:5432/fluent + BOOTSTRAP_DATABASE_URL: postgres://postgres:postgres@db:5432/fluent + MIGRATIONS_DATABASE_URL: postgres://api_migrator:password@db:5432/fluent + DATABASE_URL: postgres://api_user:password@db:5432/fluent EXPORTS_DIR: /app/exports command: [dumb-init, --, npx, tsx, watch, src/workers/standalone-worker.ts] user: '1001:1001' diff --git a/db/init/01-init-db.sql b/db/init/01-init-db.sql deleted file mode 100644 index c5e25f36..00000000 --- a/db/init/01-init-db.sql +++ /dev/null @@ -1,93 +0,0 @@ --- PostgreSQL roles and privileges for local development. --- Canonical source — fluent-ai copies from this file. --- Executed automatically on first DB volume initialization via --- docker-entrypoint-initdb.d. Safe to re-examine in psql; not re-run. - --- ================================================================ --- SECTION 1: LOGIN USERS --- ================================================================ - -CREATE USER db_admin WITH PASSWORD 'password' CREATEROLE; -CREATE USER migrations WITH PASSWORD 'password'; -CREATE USER web_user WITH PASSWORD 'password'; -CREATE USER ai_user WITH PASSWORD 'password'; - --- ================================================================ --- SECTION 2: GROUP ROLES (no login) --- ================================================================ - -CREATE ROLE role_web_data; -- full DML on public schema -CREATE ROLE role_pgboss_user; -- full DML on pgboss schema -CREATE ROLE role_ai_data; -- full DML on ai schema -CREATE ROLE role_ai_reader; -- SELECT on public schema (cross-schema reads for fluent-ai) -CREATE ROLE role_migrations; -- DDL + DML across all schemas - --- ================================================================ --- SECTION 3: ASSIGN ROLES TO LOGIN USERS --- ================================================================ - -GRANT role_web_data, role_pgboss_user TO web_user; -GRANT role_ai_data, role_ai_reader, role_pgboss_user TO ai_user; -GRANT role_migrations TO migrations; - --- ================================================================ --- SECTION 4: CREATE SCHEMAS --- ================================================================ - -CREATE SCHEMA IF NOT EXISTS drizzle; -CREATE SCHEMA IF NOT EXISTS pgboss; -CREATE SCHEMA IF NOT EXISTS ai; - --- public already exists; postgres retains ownership (superuser default). -ALTER SCHEMA drizzle OWNER TO migrations; -ALTER SCHEMA pgboss OWNER TO web_user; -ALTER SCHEMA ai OWNER TO ai_user; - --- ================================================================ --- SECTION 5: SCHEMA USAGE GRANTS --- ================================================================ - -GRANT USAGE ON SCHEMA public TO role_web_data, role_ai_reader, role_migrations; -GRANT CREATE ON SCHEMA public TO role_migrations; -GRANT USAGE ON SCHEMA drizzle TO role_migrations; -GRANT CREATE ON SCHEMA drizzle TO role_migrations; -GRANT USAGE ON SCHEMA pgboss TO role_pgboss_user; -GRANT CREATE ON SCHEMA pgboss TO role_pgboss_user; -GRANT USAGE ON SCHEMA ai TO role_ai_data; - --- db_admin: convenience DBA account for local inspection; not a runtime user -GRANT USAGE ON SCHEMA public, pgboss, ai, drizzle TO db_admin; - --- ================================================================ --- SECTION 6: DEFAULT PRIVILEGES --- Set for BOTH postgres (current Drizzle user) and migrations (future --- least-privilege target) so grants apply regardless of who runs migrations. --- ================================================================ - --- Tables created by postgres -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO role_web_data; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public - GRANT SELECT ON TABLES TO role_ai_reader; -ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public - GRANT USAGE, SELECT ON SEQUENCES TO role_web_data, role_ai_reader; - --- Tables created by migrations -ALTER DEFAULT PRIVILEGES FOR ROLE migrations IN SCHEMA public - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO role_web_data; -ALTER DEFAULT PRIVILEGES FOR ROLE migrations IN SCHEMA public - GRANT SELECT ON TABLES TO role_ai_reader; -ALTER DEFAULT PRIVILEGES FOR ROLE migrations IN SCHEMA public - GRANT USAGE, SELECT ON SEQUENCES TO role_web_data, role_ai_reader; - --- Tables created by web_user in pgboss schema -ALTER DEFAULT PRIVILEGES FOR ROLE web_user IN SCHEMA pgboss - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO role_pgboss_user; -ALTER DEFAULT PRIVILEGES FOR ROLE web_user IN SCHEMA pgboss - GRANT USAGE, SELECT ON SEQUENCES TO role_pgboss_user; - --- Tables created by ai_user in ai schema -ALTER DEFAULT PRIVILEGES FOR ROLE ai_user IN SCHEMA ai - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO role_ai_data; -ALTER DEFAULT PRIVILEGES FOR ROLE ai_user IN SCHEMA ai - GRANT USAGE, SELECT ON SEQUENCES TO role_ai_data; diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 24425d73..259d41e6 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,6 +1,9 @@ #!/bin/sh set -e +echo "Bootstrapping database roles/schemas..." +npx tsx src/db/scripts/bootstrap.ts || { echo "ERROR: db bootstrap failed"; exit 1; } + echo "Running database migrations..." npx drizzle-kit migrate || { echo "ERROR: database migrations failed"; exit 1; } diff --git a/drizzle.config.ts b/drizzle.config.ts index 22cdb39a..0d7f3780 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ out: './src/db/migrations', dialect: 'postgresql', dbCredentials: { - url: process.env.DATABASE_URL!, + url: process.env.MIGRATIONS_DATABASE_URL ?? process.env.DATABASE_URL!, }, migrations: { prefix: 'index', diff --git a/fapi.ps1 b/fapi.ps1 index 5f28931e..11d99ff9 100644 --- a/fapi.ps1 +++ b/fapi.ps1 @@ -117,7 +117,6 @@ function Start-DbContainer { --name fluent-api-db --pod $PodName ` -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=fluent ` -v fluent-api-pgdata:/var/lib/postgresql/data ` - -v "${ScriptDir}/db/init:/docker-entrypoint-initdb.d:ro" ` --health-cmd "pg_isready -U postgres -d fluent" ` --health-interval 5s --health-timeout 5s --health-retries 5 ` docker.io/postgres:16-alpine @@ -128,7 +127,9 @@ function Start-DbContainer { function Get-EnvFlags { $flags = @( "-e", "NODE_ENV=development", - "-e", "DATABASE_URL=postgres://postgres:postgres@localhost:5432/fluent", + "-e", "BOOTSTRAP_DATABASE_URL=postgres://postgres:postgres@localhost:5432/fluent", + "-e", "MIGRATIONS_DATABASE_URL=postgres://api_migrator:password@localhost:5432/fluent", + "-e", "DATABASE_URL=postgres://api_user:password@localhost:5432/fluent", "-e", "EXPORTS_DIR=/app/exports" ) if (Test-Path "$ScriptDir/.env") { $flags += @("--env-file", "$ScriptDir/.env") } @@ -340,34 +341,6 @@ switch ($Command) { else { Invoke-Compose @("exec", "db", "psql", "-U", "postgres", "-d", "fluent") } } - "db:dump-schema" { - $output = $svc - if (-not $output) { - $aiInitDir = Join-Path $ScriptDir "../fluent-ai/db/init" - if (Test-Path $aiInitDir) { $output = Join-Path $aiInitDir "02-fluent-api-schema.sql" } - else { $output = Join-Path $ScriptDir "fluent-api-schema-dump.sql" } - } - Write-Running "Dumping fluent-api public schema to $output..." - $header = @" --- Schema-only dump of fluent-api's public tables. --- Used for standalone fluent-ai development so cross-schema reads work. --- --- This file is auto-generated. DO NOT EDIT MANUALLY. --- Regenerate with: ./fapi.ps1 db:dump-schema [output-path] --- Then commit to fluent-ai/db/init/ to update the standalone DB snapshot. -"@ - if ($RuntimeMode -eq "podman-pod") { - $dump = & podman exec fluent-api-db pg_dump -U postgres --schema-only --schema=public fluent - if ($LASTEXITCODE -ne 0) { Write-Err "pg_dump failed"; exit 1 } - } else { - $dump = Invoke-Compose @("exec", "-T", "db", "pg_dump", "-U", "postgres", "--schema-only", "--schema=public", "fluent") - if ($LASTEXITCODE -ne 0) { Write-Err "pg_dump failed"; exit 1 } - } - "$header`n$dump" | Set-Content $output -Encoding UTF8 - Write-Success "Schema dumped to $output" - Write-Host "Next: commit this file to fluent-ai/db/init/ and run './fai.sh clean && ./fai.sh up' to sync." - } - "clean" { Write-Running "This will stop and remove containers and volumes." $confirm = Read-Host "Continue? [y/N]" @@ -430,7 +403,7 @@ switch ($Command) { Write-Host "Created empty .env file" } } else { Write-Host ".env already exists, skipping." } - Write-Host "Remember to fill in DATABASE_URL and BETTER_AUTH_SECRET in .env before running db:init." + Write-Host "Remember to fill in the DB URLs (BOOTSTRAP/MIGRATIONS/DATABASE) and BETTER_AUTH_SECRET in .env before running db:init." } default { @@ -464,17 +437,23 @@ Development (runs in API container): Database: db:migrate Run Drizzle migrations db:seed Seed all data (org, roles, RBAC, dev users) - db:init Run migrations + all seeds (delegates to npm run db:setup) + db:init Run migrations + all seeds (interactive confirmation) db:generate Generate a new Drizzle migration db:studio Launch Drizzle Studio on the host db:psql Open psql session - db:dump-schema [path] Dump public schema for fluent-ai standalone sync Lifecycle: clean [service] Remove containers and volumes (default: all) fresh Nuke everything: containers, volumes, and images build [service] Rebuild images without cache setup Create .env from .env.example if missing + +Environment variables: + DB_PORT Standalone DB host port (default: 5432) + API_PORT API service host port (default: 9999) + BOOTSTRAP_DATABASE_URL Superuser URL the container uses to self-provision (bootstrap) + MIGRATIONS_DATABASE_URL api_migrator URL for Drizzle migrations (DDL) + DATABASE_URL api_user runtime URL (least-privilege; set in .env to use platform DB) "@ } } diff --git a/fapi.sh b/fapi.sh index 96a9452c..9a49b502 100755 --- a/fapi.sh +++ b/fapi.sh @@ -135,7 +135,6 @@ start_db_container() { -e POSTGRES_PASSWORD=postgres \ -e POSTGRES_DB=fluent \ -v $PGDATA_VOLUME:/var/lib/postgresql/data \ - -v "$SCRIPT_DIR/db/init:/docker-entrypoint-initdb.d:ro" \ --health-cmd "pg_isready -U postgres -d fluent" \ --health-interval 5s \ --health-timeout 5s \ @@ -155,7 +154,9 @@ start_api_container() { local -a env_flags=( -e "NODE_ENV=development" - -e "DATABASE_URL=postgres://postgres:postgres@localhost:5432/fluent" + -e "BOOTSTRAP_DATABASE_URL=postgres://postgres:postgres@localhost:5432/fluent" + -e "MIGRATIONS_DATABASE_URL=postgres://api_migrator:password@localhost:5432/fluent" + -e "DATABASE_URL=postgres://api_user:password@localhost:5432/fluent" -e "EXPORTS_DIR=/app/exports" ) if [[ -f "$SCRIPT_DIR/.env" ]]; then @@ -190,7 +191,9 @@ start_worker_container() { local -a env_flags=( -e "NODE_ENV=development" - -e "DATABASE_URL=postgres://postgres:postgres@localhost:5432/fluent" + -e "BOOTSTRAP_DATABASE_URL=postgres://postgres:postgres@localhost:5432/fluent" + -e "MIGRATIONS_DATABASE_URL=postgres://api_migrator:password@localhost:5432/fluent" + -e "DATABASE_URL=postgres://api_user:password@localhost:5432/fluent" -e "EXPORTS_DIR=/app/exports" ) if [[ -f "$SCRIPT_DIR/.env" ]]; then @@ -590,35 +593,6 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then fi ;; - db:dump-schema) - output="${1:-}" - if [ -z "$output" ]; then - if [ -d "$SCRIPT_DIR/../fluent-ai/db/init" ]; then - output="$SCRIPT_DIR/../fluent-ai/db/init/02-fluent-api-schema.sql" - else - output="$SCRIPT_DIR/fluent-api-schema-dump.sql" - fi - fi - echo_running "Dumping fluent-api public schema to $output..." - { - cat <<'HEADER' --- Schema-only dump of fluent-api's public tables. --- Used for standalone fluent-ai development so cross-schema reads work. --- --- This file is auto-generated. DO NOT EDIT MANUALLY. --- Regenerate with: ./fapi.sh db:dump-schema [output-path] --- Then commit to fluent-ai/db/init/ to update the standalone DB snapshot. -HEADER - if [ "$RUNTIME_MODE" = "podman-pod" ]; then - $RUNTIME exec $DB_CONTAINER pg_dump -U postgres --schema-only --schema=public fluent - else - $COMPOSE_CMD exec -T db pg_dump -U postgres --schema-only --schema=public fluent - fi - } > "$output" - echo_success "Schema dumped to $output" - echo "Next: commit this file to fluent-ai/db/init/ and run './fai.sh clean && ./fai.sh up' to sync." - ;; - # ── Lifecycle commands ───────────────────────────────────────────────────── clean) @@ -671,7 +645,7 @@ HEADER else echo ".env already exists, skipping." fi - echo "Remember to fill in DATABASE_URL and BETTER_AUTH_SECRET in .env before running db:init." + echo "Remember to fill in the DB URLs (BOOTSTRAP/MIGRATIONS/DATABASE) and BETTER_AUTH_SECRET in .env before running db:init." ;; help|*) @@ -703,13 +677,12 @@ Development (runs in API container): run