Skip to Content
InfraLocal database

Local database (Cloud SQL proxy + migrations)

Connect your laptop to the shared Cloud SQL instance and apply Flyway SQL migrations via ./gradlew :server:dbMigrate (no full server startup, no global flyway install).

Prerequisites

  • Google Cloud SDK  (gcloud)
  • No global Flyway install requirednpm run migrate uses ./gradlew :server:dbMigrate (Flyway 11.x from Gradle). Optional: Flyway CLI  with FLYWAY_USE_CLI=1.
  • cloud-sql-proxy on PATH (install )
  • IAM: roles/cloudsql.client on project oter-490318
  • Secret Manager access to read the Postgres password secret

One-time auth:

gcloud auth login gcloud config set project oter-490318 gcloud auth application-default login

One-time: secret id in .env

The password lives in Secret Manager. You need the secret id (resource name), not the password text.

Find it (pick one):

gcloud secrets list --project=oter-490318 gcloud run services describe SERVICE \ --region=us-east1 \ --project=oter-490318 \ --format='yaml(spec.template.spec.containers[0].env)' # → DB_PASSWORD → valueFrom.secretKeyRef.name # GitHub: Repo → Settings → Actions → Variables → GCP_SM_DB_PASSWORD

Add to repo-root .env (never commit real values):

DB_SECRET_ID=your-secret-id-here

Variables (reference)

VariableRequiredDefaultPurpose
DB_SECRET_IDYes*Secret Manager secret id for Postgres password
GCP_SM_DB_PASSWORDYes*Alias for DB_SECRET_ID
DB_PASSWORDYes*Skip gcloud fetch if set directly
DB_NAMENooter_prodDatabase name
DB_USERNopostgresPostgres user
POSTGRES_HOSTNo127.0.0.1Proxy listen host
POSTGRES_PORTNo5432Proxy listen port
GCP_PROJECT_IDNooter-490318For gcloud secrets
CLOUD_SQL_INSTANCEMust be unsetIf set, server uses socket driver, not the proxy

* One of DB_SECRET_ID, GCP_SM_DB_PASSWORD, or DB_PASSWORD is required.

unset CLOUD_SQL_INSTANCE

Migrate (typical workflow)

Terminal 1 — leave running:

npm run proxy

Terminal 2:

npm run migrate

db-migrate.sh loads .env, fetches the password, then runs ./gradlew :server:dbMigrate (Flyway via JDBC, no HTTP server).

Optional Flyway CLI (if installed): FLYWAY_USE_CLI=1 npm run migrate

Manual equivalent:

unset CLOUD_SQL_INSTANCE export DB_SECRET_ID=your-secret-id source scripts/db-export-env.sh npm run migrate

Verify:

psql "postgresql://${DB_USER}:${DB_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${DB_NAME}" -c 'SELECT 1' psql "postgresql://${DB_USER}:${DB_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${DB_NAME}" \ -c "SELECT version, description, success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 10;"

Flyway CLI vs server startup

npm run migrate./gradlew :server:run
Flyway SQLYes (:server:dbMigrate)Yes (embedded)
Exposed JDBC syncNoYes
Starts HTTP serverNoYes

npm run migrate is for applying new SQL migrations to an existing database (e.g. shared oter_prod).

Server startup still runs Exposed sync + Flyway on deploy/local run. Some older migrations assume tables created by Exposed sync (e.g. V10__work_tracking.sql references work_sessions). Do not use Flyway CLI alone on a brand-new empty database unless the baseline already matches production.

Run local server against Cloud SQL

source scripts/db-export-env.sh ./gradlew :server:run

Troubleshooting

SymptomFix
flyway: command not found (manual CLI)Use npm run migrate (Gradle), or FLYWAY_USE_CLI=1 after installing CLI
password authentication failedSet DB_SECRET_ID in .env
Nothing on :5432Run npm run proxy
invalid_grant / invalid_raptgcloud auth application-default login
Flyway missing tableDB may need server once for Exposed sync, or baseline is incomplete
Server uses socket not proxyunset CLOUD_SQL_INSTANCE

Scripts

ScriptPurpose
scripts/db-proxy.shnpm run proxy
scripts/db-export-env.shEnv + Secret Manager password
scripts/db-migrate.shnpm run migrate:server:dbMigrate

See also

  • gcp-server-cloud-run.md
  • .cursor/skills/local-database-ops/SKILL.md
  • server/src/main/kotlin/com/esteban/ruano/Application.kt — Exposed sync + Flyway on server start