type: service in your grounds.yaml, one JAR, main is the entrypoint.
This page covers the two things a service can be today — a self-contained HTTP backend you push and reach at its own URL, and the in-progress gRPC domain-service model where other apps call you through a typed contract — and is honest about which is which.
A service is not the in-game minigame framework. The minigame API (
PaperMinigame, teams, phases) is plugin-side code that runs inside a game server. A service is a separate workload that plugins call out to. If you’re writing gameplay, you want a plugin; if you’re holding shared state that outlives a single round, you want a service.Service vs. plugin
The split is between where game logic runs and where shared state lives.| In-game plugin | Backend service | |
|---|---|---|
type | plugin-paper, plugin-velocity, gamemode | service |
baseImage | paper, velocity, paper-gamemode | service |
| Runtime | Paper / Velocity server | Plain JVM (main is the entrypoint) |
| Lifecycle | Joins player sessions, ticks per game | Always-on request/response |
| Owns | Gameplay, player-facing UX | Domain data, shared logic |
| Role in a call | Client — it calls services | Server — it answers, today over HTTP |
What you get when you push a service
Push atype: service app and forge gives you a plain Kubernetes Deployment plus a Service and a public HTTPS Ingress (cert-manager TLS). Your container listens on port 8080 and you get a public https:// URL. From the platform’s point of view it’s the same push pipeline as a plugin — build the image from your JAR, deploy, tail logs — just without a Minecraft runtime wrapped around it.
What this is good for today: a self-contained HTTP backend — an API your plugins hit, a webhook receiver, a small admin endpoint. You own the framework and the routes inside the JAR; Grounds owns the build, the deploy, and the public URL.
Pushing a service
A service uses the same flow as any other push — see Pushes for the full lifecycle.Declare it in grounds.yaml
Set
type: service and baseImage: service. Your JAR’s main becomes the container entrypoint — no Minecraft, no plugin loading.grounds.yaml
service is single-JAR only: the plugins: multi-JAR block is rejected for this type.Push it
grounds logs <pushId>.Configuring a service
A service is configured exactly like any other pushed app: per-deployment environment variables (shipped — use them today) and encrypted secrets (code is merged, but may not be switched on in your environment yet — secret writes return503 secrets_unavailable until the platform key is provisioned, while plain env vars keep working). Full detail, including the reserved GROUNDS_ prefix and the once-only secret reveal, lives in Environment variables and secrets.
How a plugin calls a service
Today, with a self-pushed HTTP service, your plugin reaches it the ordinary way: you have a public HTTPS URL, so make an HTTP call to it. There’s no automatic URL injection or auth wiring for arbitrary HTTP services — you hold the URL, you make the request. The richer, typed story below (declare a dependency, get a URL and a token injected, call over gRPC) is the in-progress domain-service model — read the next section before you build against it.The domain-service model (early)
There’s a more ambitious model in flight: first-class domain services that expose a typed gRPC API, that other apps discover and call through the manifest, with auth handled for you.LeaderboardService is the first vertical slice. Here’s how it’s meant to work, and exactly how far it actually reaches today.
The intended shape:
- Services publish a wire contract (
.proto) so both sides generate matching stubs. Contracts compile to JVM 21 bytecode so they load cleanly into Java 21 Paper plugins. - A consumer declares the service in a
services:block; the platform injects a<SERVICE>_SERVICE_URLenv var and a projected ServiceAccount token. Auth is k8s ServiceAccount tokens, not Keycloak — the consumer’s SDK attaches the token, the service validates it. - A plugin asks the SDK for a channel and calls a typed stub — no URL, JWT, or channel lifecycle in your code.
Talking to other apps over NATS (early)
Separately from gRPC, your pushed app can declare anevents: block to publish to (and subscribe on) the Grounds message bus. The platform parses it, stamps the matching permissions onto your app’s identity, and injects NATS_URL — the deny-by-default security layer around this is solid.
What’s proven and what isn’t:
- Publishing from your own app is infra-wired and works at that level — e.g. a gamemode push declaring
events: mobrush.results dir: pubgets the wiring to publish. - Reliable cross-app pub/sub — your app subscribing to and consuming another independently-pushed app’s events end-to-end — has not been demonstrated through the supported flow. No app → NATS → app roundtrip between two separately pushed apps exists yet.
Where to look when it misbehaves
For a pushed service, your supported, project-scoped surfaces are the same as for any app:- Runtime logs — Deployments → your app → Logs in the portal, or
grounds logs deployment <name>(--no-followto stop tailing). This tails your pod directly, scoped to your project. - Build logs —
grounds logs <pushId>. - Metrics — the stat strip on the deployment detail page in the portal. If the metrics backend isn’t configured you’ll see a graceful
503, not real numbers.
Grafana at grafana.platform.grnds.io is available for ad-hoc exploration if you have a
dev or admin role on your Grounds Account (sign in with Keycloak). It is not project-isolated today — you may land as a global Viewer and see cluster-wide data — so treat it as an operator/internal tool, not your per-app log surface. For your own app’s logs and metrics, use the portal and CLI above. See Observability for the full picture.Related
Manifest reference
The
type: service workload shape, baseImage keys, and every other grounds.yaml field.Pushes
The push lifecycle a service moves through — build, deploy, ready, retry, roll back.
Environment variables and secrets
Per-deployment config for your service — plain env vars today, encrypted secrets once the platform key is live.
Config System
A worked plugin-plus-service pair — the Config Plugin and Config API — showing the consumer side of the model.
