A Magic: The Gathering boardstate tracker built with GraphQL, Vue, and Go.
โโโ โโโโโโโโโโโโโโโโโโ โโโ โโโ
โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโ
โโโ โโโโโโโโโ โโโ โโโโโโโโโโโ
โโโโ โโโโโโโโโโ โโโ โโโโโโโโโโโ
โโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโ
โโโโโ โโโโโโโโโโโโโโโ โโโ โโโ
Pronounced "vee-dee-aych" The virtual expressive deck handler.
Prerequisites:
- Make
- Go v1.27.0
- PostgreSQL 14.15.0
- Node 20+ (CI currently uses Node 24)
Create one local environment file from the committed template. It is used by the Go server, Docker Compose, Vite, and the local monitoring stack.
cp .env.example .envReplace JWT_SECRET, database credentials, and any monitoring credentials in
.env before using the stack outside a disposable local environment.
Vite reads the root .env. The default development endpoints are already
provided by .env.example.
You can quickly start the persistence dependencies by running make persistence
This will boot up Postgres database.
Then run the server with make run, or run the API and Vite together with
make dev. Both commands load the root .env automatically.
The server will attempt to run all migrations and then start up. If a migration fails, startup stops and the database is left available for operator recovery; migrations are never automatically rolled back by the application.
You can run the server as if it's in prod with this same config, so you can switch between local and prod as long as you've configured your environment variables correctly.
A copy of the environment template is included in this repository as
.env.example.
The front end is a Vue & Apollo GraphQL application that is statically served in production.
To run the current web app:
> cd ./app
> npm install
> npm run devThe current frontend lives in app/.
npm run devto run the dev servernpm testto run the frontend unit and helper tests oncenpm run test:watchto run Vitest in watch mode
Use the smallest layer that proves the change you made:
- Frontend unit/helper tests (
cd app && npm test): proves Vue components, stores, and small browser helpers behave correctly in isolation. Runs once and exits cleanly. Requires Node and frontend deps installed; no server or database. - Backend integration tests (
make test-api): proves the Go API works against its real persistence and GraphQL paths. Requires a Postgres role withCREATE DATABASEpermission. The harness creates and drops a uniqueedhgo_test_*scratch database and uses the committed minimal card fixture; it never resets the database named byDATABASE_URL. - API smoke (Rust, preferred) (
make test-smoke-rustorcargo run --locked --manifest-path tools/smoke/Cargo.toml --): proves a create/join flow works against a running GraphQL API with minimal end-to-end setup. Defaults tohttp://127.0.0.1:8080/graphqland respectsVEDH_GRAPHQL_URL/VEDH_SMOKE_TIMEOUT_MS. For production, point it athttps://api.vedh.xyz/graphql. - API smoke (legacy JS) (
cd app && npm run test:smoke): older create/join smoke runner against a running API. Keep only as fallback while the Rust path settles. - Browser E2E (
cd app && npm run test:e2eornpm run test:e2e:headed): proves the browser experience works through real UI flows. Requires frontend deps, a running app/API target for Playwright, and browser binaries installed.
CI prepares its isolated browser-test database with make prepare-test-db.
That target applies the committed test migrations to DATABASE_URL; only use it
with a disposable database.
If you only need fast feedback, start with unit/helper tests. Reach for backend integration, the Rust smoke runner, or browser E2E when you need confidence across process boundaries.
The server emits structured JSON logs to stdout.
- Control log verbosity with
LOG_LEVEL(defaults toinfo). - Each HTTP request gets an
X-Request-Id:- If you send
X-Request-Id, the server will reuse it. - Otherwise, the server generates one and returns it in the response.
- Logs include
request_id,method,path,status, andduration_ms.
- If you send
LOG_LEVEL=debug make runFor local stack runs via dev.docker-compose.yml, add basic container log controls:
web,server, andpostgresnow use thejson-filelogging driver.- log rotation is set to
10mper file with3files retained.
Read logs per service:
docker compose -f dev.docker-compose.yml logs -f server
docker compose -f dev.docker-compose.yml logs -f web
docker compose -f dev.docker-compose.yml logs -f postgresUse this stack to validate that the API is exporting /prometheus and that Grafana is wired to it.
- Ensure API metrics are enabled and token-protected in the API env.
sed -i 's/^METRICS_ENABLED=.*/METRICS_ENABLED=true/' .env
sed -i 's/^METRICS_TOKEN=.*/METRICS_TOKEN=dev-metrics-token/' .env
sed -i 's/^PROMETHEUS_BEARER_TOKEN=.*/PROMETHEUS_BEARER_TOKEN=dev-metrics-token/' .env- Configure observability env.
Set these values in the root .env:
PROMETHEUS_TARGETto the API host:port your monitoring stack should scrape- default example:
host.docker.internal:8081for the API indev.docker-compose.yml
- default example:
PROMETHEUS_BEARER_TOKENto matchMETRICS_TOKENGRAFANA_ADMIN_USER/GRAFANA_ADMIN_PASSWORDfor UI login
- Start the stack.
make monitoring-upIf the API stack is already running, restart it after changing metrics env vars:
docker compose -f dev.docker-compose.yml up -d --force-recreate server- Verify Prometheus is running and scraping the API.
curl -sf http://localhost:9090/-/ready
curl -sf http://localhost:9090/api/v1/targets?state=any | grep -q '"job":"vedh-api"' && echo "scrape target configured"
curl -sf "http://localhost:9090/api/v1/query?query=up%7Bjob%3D%22vedh-api%22%7D" | grep -q '"value":\[' && echo "metrics query returned"- Verify Grafana has the Prometheus datasource and is connected.
export GRAFANA_ADMIN_USER=admin
export GRAFANA_ADMIN_PASSWORD=admin
curl -s -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD" \
http://localhost:3000/api/health | grep -q '"database": "ok"' && echo "grafana up"
curl -s -u "$GRAFANA_ADMIN_USER:$GRAFANA_ADMIN_PASSWORD" \
"http://localhost:3000/api/datasources/name/Prometheus" | grep -q '"url":"http://prometheus:9090"' && echo "prometheus datasource configured"Open http://localhost:3000 and run an Explore query such as:
up{job="vedh-api"}Stop the monitoring stack when done:
make monitoring-down- Postgres stores application and card data.
- Golang for the server
- Migrate CLI for managing database migrations.
- GraphQL as a realtime API layer
The repository currently contains CI verification only; there is no automatic deployment workflow. Production deployment is operator-managed.
Production is split across two Dokku apps:
appserves the frontend SPA onhttps://vedh.xyz(alsowww.vedh.xyz/app.vedh.xyz)vedh-apiserves the Go GraphQL API onhttps://api.vedh.xyz/graphql
The frontend is built with these production env vars:
VITE_GRAPHQL_HTTP="https://api.vedh.xyz/graphql"
VITE_GRAPHQL_WS="wss://api.vedh.xyz/graphql"Ensure your local deploy remote targets the frontend Dokku app when shipping app/ changes:
git remote remove dokku-app 2>/dev/null || true
git remote add dokku-app dokku@192.241.142.53:appgit add $CHANGES
git commit -m "server changes"
git push dokku maingit add $CHANGES
git commit -m "app changes"
git subtree push --prefix app dokku-app main- How to connect to a postgres instance inside of docker
- How to import an SQL dump into Postgres
- Make sure when you rows.Scan() you don't point it at a nil value
The root .env is the single local environment file. Production frontend
builds must provide VITE_GRAPHQL_HTTP and VITE_GRAPHQL_WS as Docker build
arguments or equivalent build-time environment.
The current Go server reads these env vars in code:
DATABASE_URLPORTJWT_SECRETALLOWED_ORIGINSMETRICS_ENABLEDMETRICS_TOKENLOG_LEVELDECK_IMPORT_RATE_PER_MINUTEDECK_IMPORT_RATE_BURSTDECK_PROVIDER_ENABLEDDECK_PROVIDER_ALLOWED_HOSTSGUEST_CREATION_ENABLEDGUEST_SESSION_RATE_PER_MINUTEGUEST_SESSION_RATE_BURST
/prometheus is only exposed when both of these are set:
METRICS_ENABLED=trueMETRICS_TOKENis non-empty
Even then, prefer to keep the route behind ingress/network restriction instead of relying on bearer auth alone.
Full ./server tests require a reachable Postgres role that can create a
temporary database. Test migrations seed the small committed card fixture, so
the multi-hundred-megabyte All Printings.json file is not required.
For a fast listener/origin/metrics smoke check that does not go through server/main_test.go, run:
cd /path/to/vedh
/usr/local/go/bin/go test $(find server -maxdepth 1 -name '*.go' ! -name '*_test.go' | sort) server/graphql_metrics_test.go server/graphql_origin_test.go -run 'TestGraphQLServer_|TestParseAllowedOrigins' -vUse full make test-api only when the local DB + card fixture prerequisites are available.
A few route expectations that matter when debugging prod:
https://vedh.xyzis the frontend SPA, not the GraphQL APIhttps://api.vedh.xyz/graphqlis the real live GraphQL endpointGET https://api.vedh.xyz/graphqlmay return a GraphQL validation error when no operation is supplied; that is expectedPOST https://api.vedh.xyz/graphqlis the meaningful API smoke targethttps://api.vedh.xyz/playgroundshould load the GraphQL playground- the Go API exposes
/healthz, which checks database connectivity