Praxsuite

Java SDK Implementation in Minecraft

What Are We Going to Do?

You are going to connect a Minecraft server plugin to a real backend: reading and writing rows in a cloud database, signing a player in with zero login screens, and calling server-side logic your players can never see or tamper with.

By the end of this guide you'll know:

  • Which credential to use in a Paper plugin, and why it is the opposite choice from a client-side game engine

  • How to install the SDK as a Maven dependency and shade it into your plugin's jar

  • How to sign a player in using nothing but online-mode:true — no email, no password, no browser

  • How to read and write data, and call a Gateway Endpoint

Required level: You should be comfortable writing a Paper/Spigot plugin in Java — events, commands, the plugin lifecycle. No prior experience with APIs or backends is needed.


What is Praxsuite?

Think of Praxsuite as a cloud database your plugin talks to over the internet, with a programmable layer on top of it.

A Workspace is the large folder that holds everything you build. Two pieces of it matter here: a Table is rows and columns your plugin reads and writes, and an Endpoint is logic you wrote that your plugin can only ask for — it cannot see inside it.

That difference is the whole point. Anything a cheater would want to change belongs behind an Endpoint, not in a value your plugin computes and reports.

What is a Gateway? The front door of your workspace: the single HTTPS address every request goes through. It checks your credential, applies rate limits, and passes the request on. Your plugin never talks to the database directly.


Prerequisites

Requirement

Description

JDK 17 or newer

The SDK compiles with --release 17, so it runs unmodified on Paper 1.20.x (Java 17) through 26.x (Java 25).

A Paper/Spigot plugin project

Maven, with paper-api already working. If you do not have a server to test it on yet, Part 1 of the Java SDK Use Case in Minecraft guide stands one up from scratch.

A Praxsuite workspace

Create one at portal.praxsuite.com.

One Table

Create a Table named player_profiles with columns mojang_id (ShortText) and coins (Integer).

A platform identity provider, if you plan to sign players in

A provider with slug minecraft, type "server-asserted", registered under Settings → API Gateway. See Game Platform Providers — the mechanism is identical to the one Roblox uses, just a different slug.


How to Get Your Credentials

Two values identify your plugin to Praxsuite. You need both.

Your Workspace ID

Open your workspace in the portal and look at the address bar, or the workspace selector on the portal's main menu — both show the UUID.

Your Key: publishable vs secret, and why Minecraft flips the usual advice

Key

Prefix

Where it may live

Publishable

pk_live_

Anywhere, including code a player can read. Designed to be public.

Secret

sk_live_

Server-side only. Grants whatever the key is scoped to.

On Roblox or Unity, the SDK runs on a machine you do not control — the player's — so a publishable key is usually correct there. A Paper plugin is the opposite case. It runs on a server you administer, so a secret key is normally the right choice — that is what one is for. The SDK's Praxsuite.builder() even refuses a secret key outright if you mark the build .clientSide(true), which is the setting for the rare case where you are handing this plugin to server owners who are not you.

Golden rule: the only reason to reach for a publishable key in a plugin is distributing that plugin to server operators you do not control. If it is your own server, a secret key, kept out of a public repository, is correct.


Step 1 — Add the SDK to Your Project

Repository: https://github.com/TesseractSoftwares/Praxsuite-SDK-Java

The artifact is not yet on Maven Central. Build it and install it to your local Maven repository:

cd Praxsuite-SDK-Java
./gradlew publishToMavenLocal

Then, in your plugin's pom.xml:

<dependency>
    <groupId>com.tesseractsoftwares</groupId>
    <artifactId>praxsuite-sdk</artifactId>
    <version>1.1.0</version>
</dependency>

Important — this is not `scope=provided`: unlike `paper-api`, the server does not already have this SDK loaded. It has to travel inside your own jar. Add the Shade plugin so `mvn package` bundles it:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.6.0</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals><goal>shade</goal></goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

The SDK has zero third-party dependencies of its own — it is built on java.net.http from the JDK — so what ships inside your jar stays small and cannot collide with a library another plugin already loaded into the server's classloader.


Step 2 — Create the Client

import com.tesseractsoftwares.praxsuite.Praxsuite;

public class MyPlugin extends JavaPlugin {
    private Praxsuite prax;

    @Override
    public void onEnable() {
        prax = Praxsuite.builder()
                .workspaceId("your-workspace-uuid")
                .credential("sk_live_...")
                .build();
    }
}

Praxsuite is cheap to construct and thread-safe — one instance as a field on your JavaPlugin, built once in onEnable(), is the whole pattern. (There is one legitimate reason to build a second instance — the Event Bus, covered in the Use Case guide.)


Step 3 — Sign a Player In, With No Login Screen

On Roblox, player.UserId is a verified identity because Roblox itself authenticated the player before your game server ever saw them. A Minecraft server running online-mode: true has the exact same guarantee for player.getUniqueId() — Mojang already verified the player's Microsoft account before Bukkit hands you the Player object. Praxsuite's mechanism for trusting that is assertPlayer:

PraxAuth.Session session = prax.auth().assertPlayer(
        "minecraft",                             // the provider slug configured in the portal
        player.getUniqueId().toString(),          // Mojang's UUID - the player cannot forge it
        player.getName());                        // cosmetic only

This requires a secret key marked for the `minecraft` platform in the portal — never a publishable one. The gateway trusts whoever holds that key, not the id you send, so that id has to come from a source the player cannot control (Player.getUniqueId() under online-mode:true), never a value read from a packet or a client mod.

What's actually being trusted here? Your server key, not the player. Anyone holding a `minecraft`-flagged secret key could claim to be any player, which is exactly why the gateway refuses this call with a publishable key, and why that key belongs in a config file your server owner controls, never in a distributed jar.

The one thing that is genuinely different from every other SDK: you own the session cache

Roblox's Lua SDK gives you Identify(player) and does not scope queries per player at all — the server key does everything, and per-player rules are your own code's job. The Java SDK's assertPlayer goes one step further than that and one step short of doing it all for you: it does return a real per-player session (useful for calling an Endpoint as that specific player), but it deliberately does not install that session anywhere for you.

That is not an oversight. login() does install its result as the client's one ambient session — correct for an app with a single signed-in user. A Paper plugin is not that: it asserts many players concurrently over the same shared Praxsuite instance, and if assertPlayer overwrote a shared "current session" field the way login() does, the second player to join would silently steal the first player's session, and from then on every request from anyone would run as whoever joined last. assertPlayer hands you the Session object and lets you decide where it lives:

private final Map<UUID, PraxAuth.Session> sessions = new ConcurrentHashMap<>();

@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
    Player player = event.getPlayer();
    Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
        PraxAuth.Session session = prax.auth().assertPlayer(
                "minecraft", player.getUniqueId().toString(), player.getName());
        sessions.put(player.getUniqueId(), session);
    });
}

Wherever you need to act specifically as that player later — most commonly, calling an Endpoint that assigns them a role or reads their own row — pass session.accessToken() explicitly. There is no implicit asPlayer parameter the way some sibling SDKs offer; in Java, you are always the one deciding whose token goes on the wire.

Why does a brand-new account have no access to anything? A `assertPlayer` account is created with whatever default roles the provider (or the workspace) is configured with in the portal — and if none are set, it has none, so every query it makes returns empty or 403. The Use Case guide covers a more scalable alternative: an Automation that validates the player's own token and assigns a role explicitly.


Step 4 — Read and Write Data

Every method here talks to a Table by name.

prax.data().insert("player_profiles", Map.of(
        "mojang_id", player.getUniqueId().toString(),
        "coins", 100));

Read it back:

Responses.Page page = prax.data().table("player_profiles")
        .select("mojang_id", "coins")
        .where(Filters.eq("mojang_id", player.getUniqueId().toString()))
        .limit(10)
        .fetch();

for (Map<String, Object> row : page.rows()) {
    getLogger().info(row.get("mojang_id") + " has " + row.get("coins") + " coins");
}

fetch() (and its siblings first(), count(), exists(), all()) is the only thing that actually sends the request — everything before it just builds it.

Update and delete both require a where. The SDK refuses an unscoped write before it ever reaches the network:

prax.data().update("player_profiles", Map.of("coins", 250),
        Filters.eq("mojang_id", player.getUniqueId().toString()));

Filters exposes exactly the thirteen operators the Gateway implements — eq neq gt gte lt lte like ilike in is between contains textsearch — plus a few friendly aliases (startsWith, isNull...) that compile down to one of those. There is no notIn; express it as a positive in over the values you actually want.

Column names are exact. `coins` and `Coins` are different columns, and a name with a space is not the same name with an underscore. Copy names from the portal rather than retyping them — a mismatch is the single most common reason a write "succeeds" and the value reads back as if it were never set.


Step 5 — Call a Gateway Endpoint

A Table is data; an Endpoint is logic. Whether a player can afford something, or whether they get a role, must not be a decision your plugin makes and reports — a decompiled jar is exactly as readable as a Roblox LocalScript.

Map<String, Object> result = prax.endpoints().call(endpointId, Map.of(
        "token", session.accessToken()));

call blocks until the linked Automation finishes and hands back whatever its Response node produced, parsed. Run it off the main thread — a Sync Endpoint holds the connection open while its Automation runs, and that can be tens of milliseconds your server's tick loop should never wait through.

An Endpoint does not authenticate its caller for you. A POST with no credential at all still reaches the Automation. The authority has to come from inside the Automation — most commonly a Validate End User Token node checking the token you passed, never a raw id the caller could invent. Design it that way from the start; it is the difference between "server-executed" and "server-authoritative."


Complete Example

package com.example;

import com.tesseractsoftwares.praxsuite.*;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class MyPlugin extends JavaPlugin implements Listener {

    private Praxsuite prax;
    private final Map<UUID, PraxAuth.Session> sessions = new ConcurrentHashMap<>();

    @Override
    public void onEnable() {
        prax = Praxsuite.builder()
                .workspaceId("your-workspace-uuid")
                .credential("sk_live_...")
                .build();
        getServer().getPluginManager().registerEvents(this, this);
    }

    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
            try {
                PraxAuth.Session session = prax.auth().assertPlayer(
                        "minecraft", player.getUniqueId().toString(), player.getName());
                sessions.put(player.getUniqueId(), session);

                prax.data().insert("player_profiles", Map.of(
                        "mojang_id", player.getUniqueId().toString(),
                        "coins", 0));
            } catch (PraxError e) {
                getLogger().warning("Could not open a Praxsuite session: " + e.getMessage());
            }
        });
    }
}

Run the server, join, and check the portal: a row appears in player_profiles. That round trip — Bukkit event to Gateway to database — is the whole integration.


Common Errors and How to Avoid Them

Error

Cause

Solution

PUBLISHABLE_KEY_REFUSED

assertPlayer was called on a client built with a pk_live_ credential.

Build a separate Praxsuite instance with a secret key for this call.

HTTP_403: This key is not marked for a game platform...

The key is secret, but not flagged for the minecraft platform in the portal.

Settings → API Gateway → the key → set its platform.

assertPlayer succeeds, but every query the player makes returns empty or 403

The account was created with no default roles.

Configure default roles on the provider, or assign one explicitly via an Automation (see the Use Case guide).

MISSING_WORKSPACE / MISSING_CREDENTIAL

Praxsuite.builder().build() was called without a workspace id or credential, and no PRAXSUITE_WORKSPACE_ID / PRAXSUITE_API_KEY environment variable was set either.

Pass both explicitly, or set the environment variables.

The server appears to freeze for a moment whenever a player joins or a command runs

A data()/auth()/endpoints() call ran on the main thread.

Wrap the call in Bukkit.getScheduler().runTaskAsynchronously(...), and hop back with runTask(...) before touching any Bukkit API.

Update requires a filter (no unscoped updates)

An update/delete with no condition.

Add a Filters.eq(...) (or similar) identifying the rows — this check exists so a typo cannot wipe a table.

Tip: Every SDK failure is a `PraxError` (or a typed subclass like `PraxAuthError`, `PraxRateLimitError`). Catch `PraxError` once and log `e.getMessage()` — it already says what went wrong in plain language.


Production Tips

  • Keep the secret key out of source control — a config file loaded at onEnable(), not a string literal, once you go past a personal test server.

  • Never let assertPlayer's session become the client's ambient session on the same Praxsuite instance you use for everything else — see the Use Case guide's Event Bus section for exactly why, and the pattern that avoids it.

  • Put anything a cheater would want to change behind an Endpoint whose Automation validates the caller's token — a value your plugin computes and merely reports is only as trustworthy as the jar computing it.

  • Every network call — assertPlayer, data(), endpoints() — runs off the main thread. There is no exception to this in a plugin that has more than a handful of players.

  • Column names are case- and space-sensitive. Copy them from the portal.


Next Steps

  • Read the Java SDK Use Case in Minecraft guide, where this becomes a complete Minesweeper with a locally-rendered board, a server-validated result, and a live leaderboard over the Event Bus.

  • Look at assigning roles through an Automation instead of the provider's static defaults — it scales to new platforms without touching portal configuration per platform.

  • Read Game Platform Providers for the portal-side configuration this guide assumes already exists; Step 7 of the use case guide walks through it as applied to this game.

You now have a plugin that talks to a real backend, with player identity nobody can forge. Everything after this is deciding what belongs on which side of that line.