use anyhow::{Context, Result}; use sqlx::PgPool; use sqlx::postgres::PgPoolOptions; const DEFAULT_MAX_CONNECTIONS: u32 = 10; /// SQLSTATE for `invalid_password`. const PG_INVALID_PASSWORD: &str = "28P01"; /// Turn the one connect failure with an unguessable cause into a self-explaining one. /// /// `POSTGRES_PASSWORD` is honoured ONLY when Postgres initialises its data directory. Change it in /// `.env` afterwards and the app authenticates with the new password against a volume that still /// holds the old one — a permanent restart loop whose only symptom is /// `password authentication failed`. /// /// The production secret guard makes that sequence NEARLY CERTAIN rather than rare: it stops the /// app on the first `docker compose up -d`, but not the `db` service in that same command, which /// initialises and bakes in whatever password was in `.env` at that moment. So the intended /// recovery — see the refusal, fix your secrets, boot again — is exactly the sequence that breaks /// it. Nothing in the error names the cause, and the remedy destroys data, so it is the last thing /// an operator should guess at. fn explain_auth_failure(err: &sqlx::Error) { let is_auth_failure = match err { sqlx::Error::Database(db) => db.code().as_deref() == Some(PG_INVALID_PASSWORD), _ => false, }; if !is_auth_failure { return; } tracing::error!( "Postgres rejected the credentials in DATABASE_URL (SQLSTATE {PG_INVALID_PASSWORD}).\n\ \n\ This almost always means POSTGRES_PASSWORD was changed AFTER the database volume was \ first created. Postgres applies that variable only when it initialises its data \ directory; editing .env and restarting does not change the stored password, so the two \ drift apart permanently.\n\ \n\ If the event has NOT started and you have no data worth keeping:\n\n \ docker compose down -v && docker compose up -d\n\n\ (-v DELETES the database, the uploaded media and the exports. There is no undo.)\n\ \n\ If you DO have data: restore the old password into DATABASE_URL instead, or change the \ stored one with ALTER ROLE inside the running db container. Never reach for -v to fix a \ login problem on a live event." ); } pub async fn create_pool(database_url: &str) -> Result { let max_connections = std::env::var("DATABASE_MAX_CONNECTIONS") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(DEFAULT_MAX_CONNECTIONS); let pool = match PgPoolOptions::new() .max_connections(max_connections) .connect(database_url) .await { Ok(pool) => pool, Err(e) => { explain_auth_failure(&e); return Err(e).context("failed to connect to database"); } }; sqlx::migrate!() .run(&pool) .await .context("failed to run database migrations")?; tracing::info!(max_connections, "database connected and migrations applied"); Ok(pool) }