Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.grounds.gg/llms.txt

Use this file to discover all available pages before exploring further.

The platform test environment gives every engineer their own vCluster with the full Grounds stack — Velocity proxy, plugins, lobby, gamemodes, gRPC services — running against shared backends (NATS, Postgres, Keycloak) over the service-tailnet. You override only the components you’re actively working on, leave everything else on the bundle’s defaults, and iterate with DevSpace’s hot-reload loop.
This environment is for authorized Grounds engineers only. It relies on a private Tailscale network to reach shared backend services (NATS, Postgres, Keycloak); without an invite to the Grounds tailnet you can’t follow this guide end-to-end. If you’re an external contributor working on a public component, look at the public CI flow instead.
A bundle is a versioned, curated list of platform components and their default versions. You pin against a bundle release (for reproducible CI runs) or against the current edge (for daily iteration). The full schema is documented under Bundle Reference.

TL;DR

grounds login
grounds cluster up --bundle 0.4.0
# → spins up your vCluster + deploys the full bundle stack

cp library-platform-bundle/devspace/jar-sync-pod-restart.devspace.yaml plugin-social/devspace.yaml
cd plugin-social
devspace dev plugin-social

1. Prerequisites

ToolWhyInstall
TailscaleNATS / Postgres / Keycloak are exposed only over the internal Grounds tailnet (authorized engineers only)tailscale up, accept nats.dev / postgres.dev / keycloak.dev MagicDNS routes after you’ve been added to the tailnet
kubectlHand-debug pods inside your vClusterbrew install kubectl
helmOccasional raw helm + DevSpace templates do kubectl rollout restartbrew install helm
devspaceThe hot-iteration loopbrew install devspace
gradle + JDK 25Building plugin / service JARs locallybrew install gradle openjdk@25
grounds-cliControl plane for your workspaceSee CLI installation
Sanity check:
grounds doctor
It validates auth, API reachability, and Tailscale routes for *.dev.

2. Bootstrap your vCluster

grounds login                       # OAuth device flow against account.grounds.gg
grounds cluster up --bundle 0.4.0
What this does:
1

Resolve project

Forge resolves your default project on the platform side.
2

Ensure host namespace

A per-engineer host namespace vcluster-<handle> (privileged PSA) is created if missing.
3

Install vCluster

helm install of loft/vcluster, idempotent — re-runs are no-ops if the vCluster already exists.
4

Wait for vCluster API

The reconciler polls until the inner API is up and pulls its kubeconfig out of a Secret.
5

Resolve bundle

The forge service fetches bundle.yaml @ 0.4.0 and merges it with your override file (if any) into a concrete component list.
6

Deploy components

Each component is helm install’d into the vCluster, best-effort — a chart-not-found on one component doesn’t abort the rest.
The output tells you which components landed and which ones errored:
✔ active — bundle 0.4.0 — vcluster-lusu
  components: 15 resolved, 14 succeeded, 1 failed
  failed:
    - paper-game: chart oci://ghcr.io/groundsgg/charts/grounds-gamemode:0.1.0 not found
Re-run grounds cluster up --bundle 0.4.0 after fixing the cause and the reconciler picks up where it left off.
Bundle pin vs. main--bundle 0.4.0 is reproducible: that exact composition gives you the same component versions every time. --bundle main always tracks the moving edge. Pin in CI, float locally.

3. Get your vCluster’s kubeconfig

For raw kubectl / helm / devspace access to the inner cluster, the kubeconfig lives in a Secret on the host namespace:
HANDLE=$(whoami)
kubectl --context=platform-cluster -n vcluster-$HANDLE \
  get secret vcluster-$HANDLE -o jsonpath='{.data.config}' \
  | base64 -d > ~/.kube/config-vcluster-$HANDLE
export KUBECONFIG=~/.kube/config-vcluster-$HANDLE
kubectl get pods -n default
Each engineer’s vCluster runs separately on the platform cluster — your default namespace inside the vCluster is your stack.
A first-class grounds cluster kubeconfig subcommand is planned but not yet shipped — for now the manual fetch above is the supported path.

4. Override a component

Without overrides, every component runs the released image at the version pinned by the bundle. To swap one for your local code, write an override file:
overrides/me.yaml
bundle: 0.4.0

overrides:

  # Build plugin-social locally, sync the JAR into the running pod.
  plugin-social:
    mode: gradle-local
    project: ./plugin-social
    artifact: build/libs/plugin-social-*.jar
    reload: pod-restart       # rolls Velocity so it picks the new JAR

  # Run service-config in Quarkus dev mode (sub-second hot-reload).
  service-social:
    mode: gradle-local
    project: ./service-social
    devspace: true            # → quarkus-dev workflow

  # Engineer wants a specific velocity tag, not the bundle default.
  velocity:
    mode: image
    tag: dev-fix-routing-123

  # Skip paper-game entirely in this stack.
  paper-game:
    enabled: false

  # Turn on optional test tooling.
  nats-recorder:
    enabled: true
Apply it:
grounds cluster up --bundle 0.4.0 --override ./overrides/me.yaml

Override schema

ModeWhenRequired fields
image (default)Run the bundle’s default image
image + tag: <X>Same image, different tagtag
gradle-localLocal JAR syncproject, artifact, reload
gradle-local + devspace: trueQuarkus dev modeproject
enabled: falseSkip the component
enabled: trueTurn on a default-off optional component
See Bundle Reference for the full override schema and field-level documentation.

5. Iterate on a component

The bundle’s per-component devspace.workflow field selects which template applies:
Component-TypeDevSpace WorkflowIter-Latency
plugin-velocityjar-sync-pod-restart (Velocity has no in-process hot-reload, so the pod is restarted)~10–20 s
gamemode (Minestom/Paper)jar-sync-pod-restart~10–20 s
grpc-service (Quarkus)quarkus-dev (Live-Reload sub-second)~1–3 s
Use jar-sync-pod-restart:
cp library-platform-bundle/devspace/jar-sync-pod-restart.devspace.yaml plugin-social/devspace.yaml
cd plugin-social
devspace dev plugin-social \
  --var COMPONENT_NAME=plugin-social \
  --var PROJECT_DIR=. \
  --var JAR_GLOB="build/libs/*-all.jar" \
  --var RESTART_TARGET=velocity
DevSpace will:
  1. Run ./gradlew build locally.
  2. Sync the produced JAR into the plugin pod’s /shared/plugin.jar.
  3. kubectl rollout restart deployment/velocity — Velocity’s init-containers re-fetch your JAR.
  4. Watch build/libs/ and re-do all of the above on every change.

6. Inspect the event bus

The bundle has an opt-in nats-recorder component that subscribes to chosen subjects and emits each message as JSONL on stdout — useful for debugging cross-service events without writing one-off subscribers.
# overrides/me.yaml
overrides:
  nats-recorder:
    enabled: true
After grounds cluster up, tail it:
kubectl logs -f deployment/nats-recorder | jq .
Subjects it listens on are configured in bundle.yaml (default: config.>, player.>, friends.>, match.>).

7. Tear down

grounds cluster down            # pause; resume with `cluster up`
grounds cluster delete          # drop the vCluster + namespace entirely
down is cheap to reverse; delete requires re-running the bundle deploys from scratch. To swap one component out without losing state, use overrides — don’t delete.

8. Troubleshooting

The Helm chart for that component type isn’t published yet. The forge response’s failed[] block tells you exactly which component and which chart URL failed. Either wait for the chart to be released or override the failing component with enabled: false so the rest of the stack still comes up.
The grounds-velocity chart’s init-containers curl each plugin’s Service at http://<plugin>:8080/plugin.jar. If a plugin pod isn’t ready (e.g. its image doesn’t exist or it’s still pulling), the velocity pod waits. kubectl get pods and look for the plugin pods that aren’t Running.
The jar-sync-pod-restart workflow assumes you set RESTART_TARGET=velocity for plugin-* components. Without the rollout-restart, Velocity keeps the old JAR loaded. Check the onUpload.execRemote block in your devspace.yaml.
You’re not on the internal Grounds tailnet. This is a private network for authorized engineers — if you haven’t been invited yet, ask a maintainer. Once you’re on, run tailscale status to confirm the routes; if they’re missing, re-run tailscale up accepting the dev-routes ACL.
You already have a profile=platform workspace from the older flow. The bundle workflow uses profile=platform-bundle and they’re not interchangeable. grounds cluster delete first, then re-up with --bundle.
grounds cluster up --bundle X is idempotent and uses helm install/upgrade. If a chart’s values shape changed between bundle versions, a stale release can block the upgrade. Drop the specific component’s release inside the vCluster (helm uninstall <component> against the vCluster kubeconfig) and re-run cluster up.

9. Reference