Skip to main content

Read-only access

grafana_reader is a Postgres role that can read every table and change nothing. Grafana's FantasyFootballDB datasource uses it, and you can use it to inspect data without risking a write. Use it instead of bot_admin for any query you don't intend to change data with.

What can the role do?​

CapabilityGranted
Connect to the fantasy_football databaseYes
SELECT on every table in the public schemaYes, including tables created later
INSERT, UPDATE, DELETE, DDLNo

Core's migrations run as bot_admin. A default privilege on that role's new tables grants SELECT to grafana_reader automatically, so a new table needs no extra step.

It reads everything

The grant is for the whole schema. That includes activity_actions.payload, execution_log.error_message and llm_call_metrics. Secret-looking keys are redacted from stored activity payloads, but treat query output as internal, and don't paste it into public issues.

Where does the role come from?​

The role is defined in fantasy-infra, not in this repo: base/databases/postgres-readonly-role-job.yaml. The Job runs once as bot_admin and is safe to re-run; it sets the password to the current OpenBao value each time.

ItemValue
Rolegrafana_reader
Databasefantasy_football
PasswordOpenBao key fantasy/postgres-readonly, property readonly-password
Provisioned byfantasy-infra, base/databases/postgres-readonly-role-job.yaml
Consumed byGrafana datasource FantasyFootballDB in homelab-deployment

How do I connect for debugging?​

Open a psql session from your machine

You need a way to reach the database host, such as a port-forward to the Postgres service. Then read the password from OpenBao (see the bao-login workflow) and connect:

export PGPASSWORD="$(bao kv get -field=readonly-password secret/fantasy/postgres-readonly)"
psql -h <db-host> -U grafana_reader -d fantasy_football

Replace <db-host> with the Postgres host or your forwarded localhost. Don't put the password on the command line or in a file.

What can I query?​

Dashboards in fantasy-infra (base/core-pipeline/*-dashboard.yaml) read from these tables.

DashboardMain tables
Execution logexecution_log
Bot decisionsbot_decisions
Backtestsbacktest_results
Draft pipelinedraft_picks, draft_slots, draft_config, execution_log

Example queries​

Recent worker failures
SELECT executed_at, transaction_type, bot_name, error_message
FROM execution_log
WHERE status IN ('failure', 'partial_failure')
ORDER BY executed_at DESC
LIMIT 20;
Why did we make this lineup change?
SELECT decided_at, action, target_player_id, drop_player_id,
season, week, inputs
FROM bot_decisions
WHERE tracking_id = '<tracking-id>';
Top VORP this week
SELECT p.name, p.position, w.projected_points, w.vorp_score
FROM weekly_stats w
JOIN players p USING (player_id)
WHERE w.season = 2026 AND w.week = 6
ORDER BY w.vorp_score DESC
LIMIT 25;
Incoming trade offers awaiting a decision
SELECT id, status, counter_offer->>'espn_transaction_id' AS tx,
counter_offer->'score'->>'final_score' AS score
FROM execution_log
WHERE transaction_type = 'trade_inbox' AND status = 'open'
ORDER BY executed_at DESC;
tip

Always filter weekly_stats on both season and week. Join on player_id only for Sleeper ids; use players.espn_id to match ESPN ids, and never compare the two spaces directly.

  • Data model: the tables and their JSONB columns.
  • Migrations: why new tables become readable on their own.