Astral Realms Documentation Help

Metabase

AstralAnalytics ships a docker-compose.yml that boots PostgreSQL and Metabase side-by-side. Use it to visualise the events table without writing a single line of code.

Boot

docker compose up -d

Services:

Service

Container

Port

Volume

Postgres 15

metabase_db

5432

postgres_data

Metabase

metabase_app

3000

metabase_data

Both join the metabase_net bridge network so Metabase resolves the database as db.

The shipped credentials are placeholders — change POSTGRES_PASSWORD and MB_DB_PASS before exposing the stack beyond localhost.

First-run setup

  1. Open http://localhost:3000.

  2. Follow the Metabase wizard for your admin account.

  3. Add a database of type PostgreSQL pointing at the events database the master writes to:

    • Host: db from inside the Docker network, or your DB host externally.

    • Database name: metabase (or whatever you configured).

    • Username / password: as set in docker-compose.yml.

  4. Metabase syncs and surfaces the events table.

Dashboard building blocks

The events table is intentionally narrow — every dashboard reads created_at, type, player_uuid, and the payload JSONB.

Active players

SELECT date_trunc('day', created_at) AS day, COUNT(DISTINCT player_uuid) AS players FROM events WHERE type = 'player_join' GROUP BY day ORDER BY day;

Render as a time-series line chart.

Country breakdown

SELECT payload->>'country' AS country, COUNT(*) AS joins FROM events WHERE type = 'player_join' AND created_at > {{date_range}} GROUP BY country ORDER BY joins DESC;

Render as a pie or world map.

Session length distribution

SELECT WIDTH_BUCKET((payload->>'session_duration')::bigint, 0, 3600000, 12) AS bucket_minutes, COUNT(*) AS sessions FROM events WHERE type = 'player_quit' GROUP BY bucket_minutes ORDER BY bucket_minutes;

Render as a histogram. Buckets are 5-minute bands up to the first hour; anything longer falls in bucket 13.

Top commands

SELECT split_part(payload->>'command', ' ', 1) AS command, COUNT(*) AS uses FROM events WHERE type = 'command_execute' AND created_at > NOW() - INTERVAL '24 hours' GROUP BY command ORDER BY uses DESC LIMIT 25;

Render as a bar chart.

Mod presence

SELECT modinfo->>'id' AS mod_id, modinfo->>'version' AS version, COUNT(*) AS players FROM events, LATERAL jsonb_array_elements(payload->'mods') AS modinfo WHERE type = 'player_mod_info' GROUP BY mod_id, version ORDER BY players DESC;

LATERAL jsonb_array_elements flattens the per-player mod arrays so each mod is one row.

LuckPerms promotion ladder

SELECT payload->>'from' AS from_group, payload->>'to' AS to_group, COUNT(*) AS count FROM events WHERE type = 'luckperms-promote' GROUP BY from_group, to_group ORDER BY count DESC;

Render as a Sankey or simple table.

Tips

  • GIN index: the schema includes CREATE INDEX … USING GIN (payload). Use the JSON containment operator @> (payload @> '{"country": "FR"}') to take advantage of it instead of casting.

  • Filtering by server: server_name and server_group are top-level VARCHARs. Add them as table filters when you want per-shard or per-environment slices.

  • Time-series with gaps: PostgreSQL's generate_series is handy for filling missing days when the raw date_trunc query has zero rows for a slot.

  • Materialise common rollups: if a dashboard's query starts to feel slow, push it into a materialised view and refresh it on a cron. The events table is append-only so rollups stay simple.

Last modified: 25 July 2026