Server and Java
- Java 17 or newer for building and running Graaly.
- A plugin-capable server from 1.7.10 through 26.2.
- The legacy-launcher agent from the release only when an old launcher rejects your modern JVM.
Use one language-native API from Minecraft 1.7.10 through 26.2. Graaly handles release differences and downloads verified language runtimes only when they are needed.
Start here before copying an example. Graaly itself runs on Java 17 or newer; choose a JVM version that also satisfies your server release. Node.js and Python are development tools, not separate in-server runtimes.
.mjs entry file.graaly or @graaly/react.async/await.Download Graaly-1.0.0.jar and, only for historical launchers, the matching legacy-launcher agent.
Place only the runtime JAR in plugins/; keep server and PacketEvents JARs separate.
Start once. Graaly downloads the enabled runtimes to plugins/Graaly/runtime/25.2.4/, then loads bundles from plugins/Graaly/scripts/.
GraalJS and GraalPy are not inside the plugin JAR. Graaly downloads only the enabled languages from Maven Central, checks exact size and SHA-256, and reuses the verified cache on every restart.
auto-download to false.runtime:
auto-download: true
languages:
javascript: true
python: true
connect-timeout-seconds: 20
request-timeout-seconds: 180
retry-attempts: 2Your public imports and autocomplete stay fixed from 1.7.10 to 26.2. Version-specific names, signatures, and fallbacks live behind Graaly's adapter boundary.
Materials.GRASS_BLOCKcontract 1.0faithful native valueThe same imports, modules, properties, and canonical constants stay in autocomplete on every supported release.
Graaly maps historical names and method shapes inside its adapter. Plugin code never selects a server version.
If an old release cannot represent a feature, Graaly reports it through compatibility and GraalyUnsupportedFeature.
import { Attributes, Materials, compatibility } from "graaly";
const floor = Materials.GRASS_BLOCK; // same name everywhere
if (compatibility.supports("attributes")) {
const health = Attributes.MAX_HEALTH;
}from graaly import Attributes, Materials, compatibility
floor = Materials.GRASS_BLOCK # same name everywhere
if compatibility.supports("attributes"):
health = Attributes.MAX_HEALTHoff_handavailable from 1.9attributesavailable from 1.9entity_aiavailable from 1.9entity_invulnerableavailable from 1.9glowingavailable from 1.9elytraavailable from 1.9entity_gravityavailable from 1.10persistent_dataavailable from 1.14custom_model_dataavailable from 1.14allayavailable from 1.19display_entitiesavailable from 1.19.4data_componentsavailable from 1.20.5bundlesavailable from 1.21.2dialogsavailable from 1.21.6Install Graaly once, create one plugin bundle, copy it to the documented server directory, and verify that the loader discovered it. Every path below is executable, not just illustrative.
Graaly does not currently publish a release JAR. Build the current source, copy the single runtime JAR into your server's plugins/ directory, and start the server once.
Java 17+ and Maven produce runtime/target/Graaly-1.0.0.jar.
Copy only that JAR to server/plugins/. The legacy agent is not a plugin.
Graaly creates its directory and downloads the enabled GraalJS/GraalPy runtimes.
Check the ready log, then run graaly status from the console.
git clone https://github.com/sk8erboi17/Graaly.git
cd Graaly/runtime
mvn clean package
# Copy the runtime, not the legacy agent, into the server:
cp target/Graaly-1.0.0.jar /absolute/path/to/server/plugins/
cd /absolute/path/to/server
java -jar server.jar noguiserver/
├── server.jar
└── plugins/
├── Graaly-1.0.0.jar
└── Graaly/
├── config.yml
├── runtime/25.2.4/
└── scripts/[Graaly] Graaly is ready: 0 script plugin(s), downloaded Graal …In the server console run graaly status. In game use /graaly status as an operator or with graaly.admin permission.
The selected tab changes the source, manifest, build command, destination directory, and load instructions together.
name: WelcomeTS
version: 1.0.0
main: dist/main.mjs
commands:
hello:
description: Say helloimport { commands, events, PlayerJoinEvent }
from "graaly";
events.on(PlayerJoinEvent, event => {
event.joinMessage = `§b${event.player.name} joined`;
event.player.sendMessage("&bWelcome!");
});
commands.on("hello", context => {
context.reply(`&bHello ${context.sender.name}`);
return true;
});node_modules. Python deploys source and runs on GraalPy. Java deploys a compiled JAR.The whole bundle is one directory. Its suffix tells Graaly which language loader to use, and plugin.yml points to the entry file inside it.
Run the language toolchain locally; do not install Node.js or CPython on the game server.
Place the bundle under plugins/Graaly/scripts/.
Follow the load rule below, check the log, then execute /hello in game.
cd /absolute/path/to/Graaly/runtime/examples/TypeScriptHello.jsplugin
npm ci
npm run build
# Output: dist/main.mjsmkdir -p /absolute/path/to/server/plugins/Graaly/scripts/WelcomeTS.jsplugin/dist
cp plugin.yml /absolute/path/to/server/plugins/Graaly/scripts/WelcomeTS.jsplugin/
cp dist/main.mjs /absolute/path/to/server/plugins/Graaly/scripts/WelcomeTS.jsplugin/dist/plugins/Graaly/scripts/WelcomeTS.jsplugin/
├── plugin.yml
└── dist/
└── main.mjsFor a new bundle, restart the server. After changing code in an already loaded bundle, rebuild, copy dist/main.mjs, then run graaly reload in the server console or /graaly reload in game.
plugin.yml change: restart the server. Source-only change to an already loaded bundle: copy the rebuilt file, then use /graaly reload as an operator.Graaly gives ordinary language constructs useful game-server data. Follow one path at a time, copy the complete example, then recognize what belongs to the language, what belongs to Graaly, and what only exists in a browser or Node.js environment.
Keywords, modules, collections, control flow, types, exceptions, and async syntax.
Players, worlds, events, commands, tasks, entities, and packets.
Live state on Java 17 or newer. This is not a web page or a Node.js process.
Graaly runs server-side, so browser globals such as window, document, and HTMLElement are intentionally absent there. Website boards are still experimental and remain outside the stable public contract until their browser lifecycle and input behavior are ready.
Import explicit Graaly names and alias modules only when it improves clarity.
from graaly import players
def online_names() -> list[str]:
return [player.name for player in players]from graaly import (
CommandContext, PlayerJoinEvent, command, event, players, tasks,
)
@event(PlayerJoinEvent)
async def welcome(event: PlayerJoinEvent) -> None:
others = [player.name for player in players
if player != event.player]
await tasks.sleep_ticks(20)
event.player.send_message(
f"Online now: {', '.join(others) or 'only you'}"
)
@command("team")
def team(context: CommandContext) -> bool:
match context.args:
case ["list"]:
context.reply(", ".join(p.name for p in players))
case ["find", name] if (target := players.exact(name)) is not None:
context.reply(f"Found {target.name}")
case _:
return False
return Truefrom collections.abc import Iterator
from contextlib import contextmanager
from graaly import Player, info, players
def online_names() -> Iterator[str]:
for player in players:
yield player.name
@contextmanager
def temporary_health(player: Player, value: float) -> Iterator[Player]:
previous = player.health
try:
if value < 0:
raise ValueError("health cannot be negative")
player.health = min(value, player.max_health)
yield player
except ValueError as error:
info(f"Invalid health: {error}")
raise
finally:
player.health = previous
def show_temporary_health(player: Player) -> None:
with temporary_health(player, 20.0) as healed:
healed.send_message(", ".join(online_names()))info(value) writes an INFO line to this plugin's server logger; warn(value) writes a warning. They do not send text to players.An event is a notification from the server. Pick the event that describes the moment you care about, register a listener, read its data, then optionally change or cancel the action.
A player moves, a block breaks, an entity takes damage, or a world loads.
The callback receives one typed event object. No string resolver is needed.
Read properties, change editable values, or set cancelled when supported.
265 events
A player is about to break a block, before the server removes it.
Use it for region protection, custom drops, building rules, and block mechanics. This event also lets you change expToDrop, dropItems.
block, expToDrop, cancelled, player, dropItems
expToDrop, cancelled, dropItems
blockBlockexpToDropnumbereditablecancelledbooleaneditableplayerPlayerdropItemsbooleaneditableBlockBreakEvent · @EventHandlerimport { BlockBreakEvent, events, Material } from "graaly";
events.on(BlockBreakEvent, event => {
if (event.block.type === Material.DIAMOND_ORE
&& !event.player.hasPermission("mine.diamond")) {
event.cancelled = true;
event.player.sendMessage("You cannot mine this ore.");
return;
}
event.expToDrop = 0;
});Graaly maps this event directly to its Java listener type.
Search by goal, choose a task, and see what it does before the code. Every guide includes native JS, TS, and Python plus the exact Java pattern it replaces.
10 practical guides
Use this after declaring the command name in plugin.yml.
Runs your callback with the sender, label, arguments, reply helper, and permission helper already adapted.
Command name and a callback
true when handled; false to show plugin.yml usage
commands.on(name, handler)context.senderplayers.isPlayer(sender)context.reply(message)context.hasPermission(node)JavaPlugin#getCommand(String) → PluginCommand#setExecutor(CommandExecutor)import { commands, players } from "graaly";
commands.on("graaly", context => {
if (!players.isPlayer(context.sender)) {
context.reply("&cOnly players can use this command.");
return true;
}
const player = context.sender; // narrowed to Player
if (!context.hasPermission("graaly.admin")) {
context.reply("&cYou do not have permission.");
return true;
}
player.sendMessage(`§aHello ${player.name} from Graaly`);
return true;
});Important: context.sender is always the command sender, but it is not necessarily a Player: it may be the console or a command block. players.isPlayer(context.sender) safely narrows it to Player. players.broadcast(...) only sends a message to every online player; it does not return the player who ran the command.
Find players, message them, change inventory or health, teleport, control movement, permissions, scoreboards, metadata, time, and weather. The complete inherited Player catalog remains one click away.
9 practical guides
Start here whenever a command, task, or service needs a Player object.
Returns native Player values, not handles that require unwrapping.
Optional player name
Iterable<Player>, Player, or null/None
players or players.online()players.get(name)players.exact(name)Bukkit#getOnlinePlayers / getPlayer / getPlayerExactimport { players } from "graaly";
const names = [...players].map(({ name }) => name);
for (const online of players) {
online.sendMessage(`Online as ${online.name}`);
}
const steve = players.exact("Steve");
if (steve) steve.sendMessage("Found you");Location is now a first-class guide beside blocks, chunks, time, weather, spawn rules, effects, world creation, custom generators, populators, saving, and unloading.
9 practical guides
Use it anywhere the Java API expects a Location: teleporting, spawning, effects, blocks, or distances.
Creates the native Location while your code stays idiomatic JS, TS, or Python. Coordinates must be real finite numbers and conversions may not lose integer precision.
World, x, y, z, optional yaw and pitch
Location
worlds.location(world, x, y, z, yaw, pitch)location.blocklocation.chunknew org.bukkit.Location(World, double, double, double, float, float)import { info, worlds } from "graaly";
const world = worlds.get("world");
if (world) {
const location = worlds.location(world, 12.5, 70, -4.5, 90, 0);
info(`${location.blockX}, ${location.blockY}, ${location.blockZ}`);
}info(value) writes an INFO line to this plugin's server logger; warn(value) writes a warning. They do not send text to players.Search the exact action you need. The attribute guide includes all 40 canonical attributes from the current contract and tells you which releases can represent each mechanic faithfully.
9 practical guides
Use it for custom mobs, NPC-like mechanics, projectiles, and arena entities.
Graaly selects the correct native spawn path, while EntityTypes.ZOMBIE, HORSE, ITEM, and every other constant preserve their TypeScript and Python return type.
Location, EntityTypes value, and optional configuration
Precisely inferred Zombie, Horse, Item, or other entity subtype
entities.spawn(location, type, options)EntityTypes.ZOMBIEWorld#spawnEntityimport { entities, EntityTypes } from "graaly";
const zombie = entities.spawn(location, EntityTypes.ZOMBIE, {
name: "§aGraaly guardian",
nameVisible: true,
});Build small menus with familiar tags while keeping a faithful game result: inventory grids and stained-glass panels, sign-based text input, and two-choice anvil dialogs. The same compiler and action model are exposed to TypeScript, JavaScript, and Python.
div, span, button, input, and dialog. Graaly parses them on the server and creates Minecraft inventory, sign, and anvil screens; it does not send HTML to the client.div · main · section · formNative panel/container; its grid area groups child elements.
span · p · label · h1…h6Text item: content becomes the item display name and tooltip.
buttonClickable inventory item; id or data-action selects the handler.
inputClickable field that opens the native four-line sign editor.
dialog openAnvil modal; exactly two button children occupy slots 0 and 1.
hrOne horizontal stained-glass line across the current grid row.
import { commands, players, ui } from "graaly";
const menu = `
<div id="profile" aria-label="Profilo" data-rows="3">
<style>
#profile {
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-template-rows: repeat(3, 1fr);
background: white;
border: 1px solid black;
border-radius: 8px;
}
span { grid-column: 2 / 9; grid-row: 1; color: yellow; }
input { grid-column: 3 / 8; grid-row: 2; background: lightblue; }
button { grid-column: 4 / 7; grid-row: 3; --minecraft-material: EMERALD_BLOCK; }
</style>
<span>Configura il profilo</span>
<input id="player-name" placeholder="Scrivi sul cartello">
<button id="confirm">Conferma</button>
</div>`;
commands.on("htmlgui", context => {
if (!players.isPlayer(context.sender)) return true;
const player = context.sender;
ui.renderHtml(player, menu, { actions: {
"player-name": action => player.sendMessage(`Testo: ${action.value ?? ""}`),
confirm: () => ui.renderHtml(player, `
<dialog id="save" open aria-label="Sei sicuro?">
<button id="yes" style="background: lime">Sì</button>
<button id="no" style="background: red">No</button>
</dialog>
`, { actions: {
yes: () => player.sendMessage("Confermato"),
no: () => player.sendMessage("Annullato"),
}}),
}});
return true;
});from graaly import command, players, ui
@command("pyhtmlgui")
def open_gui(context):
if not players.is_player(context.sender):
return True
ui.render_html(
context.sender,
'<input id="name" placeholder="Scrivi sul cartello">',
css='input { background: white; }',
actions={"name": lambda action: context.reply(action.value or "")},
)
return Truegrid-template-rows, grid-row, and grid-column map to 9 columns and 1–6 inventory rows.
Named, hex, and rgb() colors map to the nearest dye. white is WHITE_STAINED_GLASS_PANE.
border paints the perimeter; border-radius leaves the four corner slots empty for a rounded-panel shape.
color, font-weight, font-style, and text decoration become Minecraft formatting codes.
--minecraft-material, --minecraft-amount, --minecraft-durability, and --minecraft-lore select item details.
Use data-action on controls and data-close-action on a container. Scripts, DOM APIs, and inline onclick are rejected.
Follow the animated IDE from the first React root to a tested FastAPI transaction. Every lesson explains the code, how it differs from browser React, why that boundary was chosen, which simpler alternative exists, and when the additional architecture becomes justified.
Follow one architectural decision at a time. Compare game UI with web React, inspect the code, and learn why each boundary exists.
import React from "react";Your component still returns JSX and React still reconciles state. Graaly implements a renderer that turns that tree into native game UI.
The genuine React programming model transfers to web work: components, props, state, Hooks, Context, composition, and one-way data flow all keep their meaning.
A website calls react-dom createRoot on an HTMLElement and produces DOM nodes. Graaly calls createRoot(player) and produces a scoreboard, inventory, boss bar, tab list, or message for that player.
The React renderer uses explicit native host elements because there is no DOM inside the game process. For small non-React menus, the separate ui.renderHtml helper compiles semantic HTML and a limited CSS grid into the same native snapshots.
@graaly/react uses React 19 and its reconciler. Components, JSX, props, hooks, context, state, effects, composition, error handling, and third-party state libraries keep their normal meaning. The only difference is the render target: game UI instead of an HTML DOM.
Join, command, inventory click, packet, or your own rule.
Keeps the player reference and publishes safe UI actions.
State decides what each player sees; updates are reconciled.
Validates requests and persists data with SQLAlchemy.
Receives live events, owns player references, and applies UI or world changes on the safe server thread.
Owns HTTP routes, validation, authentication, database sessions, migrations, and business data.
FastAPI can be your application backend, but it cannot directly receive in-process game events or hold live player objects. The small Graaly adapter is the boundary that makes the split reliable.
Graaly uses the long service key only to exchange a trusted player identity for a short-lived JWT. Every protected request carries that bearer token, while FastAPI reloads the actor's current roles and permissions from SQL before running the endpoint. A role change therefore revokes access immediately, even when the old identity token has not expired.
POST /v1/auth/session accepts the service key and player identity.
The token identifies the player; it does not freeze a permission snapshot.
Member, moderator, and admin roles resolve to current permission keys.
require_permission(...) allows or rejects before business logic.
profile.read, shop.purchase, and realtime.connect.
Profile and realtime access plus permissions.read, without role mutation.
All permissions, including permissions.manage. Bootstrap admins cannot remove their own emergency access.
Use Graaly for rules and data, plus React for UI. No HTTP service is required.
Use decorators, asyncio, and ui.render. React is optional, not mandatory.
React/TS owns presentation; FastAPI/SQLAlchemy owns durable application data.
<Message>One-shot chat, action-bar, or title output. Give it a stable unique id.
<Inventory> + <Item>Menu slots, names, lore, amounts, click handlers, and close handlers.
Java: Inventory + inventory events<Scoreboard> + <Line>Up to 15 keyed lines. Stable IDs let Graaly update only changed rows.
Java: Scoreboard + Objective + Team<BossBar>Text and progress from 0 to 1, updated only when the values change.
Java: legacy 1.8 boss display packets<Tab>Per-player header and footer that follow normal React state.
Java: setPlayerListHeaderFooter<ChatInput>A controlled or uncontrolled text field backed by the player's next chat message, with submit, cancel, and cleanup.
React: value / defaultValue / onSubmitcreateRoot(player)Creates one isolated React root. Call unmount() when the player leaves.
ref + handleTyped Inventory, Scoreboard, BossBar, Tab, and ChatInput handles expose only dismiss, refresh, ID, kind, and current props.
React 19: ref prop + imperative APIcreatePortal(...)Targets another player root while preserving the source component's Context and logical ownership.
React: genuine cross-root portalroot.getCommits()Reads immutable native-surface diffs, durations, and history so performance and batching can be proven.
Graaly: commit inspector@graaly/react-testUses the real reconciler and act() to click slots, submit input, inspect snapshots, and assert commits.
Read each pair from left to right: the React pattern controls presentation and interaction; the FastAPI pattern protects data and business rules. Every example is ordinary framework code.
Transport details stay in one module, while both sides agree on the data crossing the process boundary.
Component calls a domain method
Client serializes the command
FastAPI validates it
A typed result returns
import { config, http, type HttpResponse } from "graaly";
const API_URL = "http://127.0.0.1:8000";
const apiKey = String(config.get("backend.api-key"));
const options = { headers: { "x-graaly-key": apiKey } };
export type Profile = {
id: string; name: string; rank: string; coins: number;
};
export type PurchaseCommand = {
playerId: string; playerName: string; item: "diamond" | "gold";
};
export type PurchaseResult = { profile: Profile; message: string };
class ApiError extends Error {
constructor(readonly status: number, message: string) { super(message); }
}
async function read<T>(response: HttpResponse): Promise<T> {
const body = await response.json<T | { detail: string }>();
if (!response.ok) {
const message = typeof body === "object" && body !== null && "detail" in body
? String(body.detail) : `HTTP ${response.status}`;
throw new ApiError(response.status, message);
}
return body as T;
}
export const api = {
player: (id: string, name: string) =>
http.get(`${API_URL}/v1/players/${encodeURIComponent(id)}/ui?name=${encodeURIComponent(name)}`, options)
.then(read<Profile>),
purchase: (command: PurchaseCommand) =>
http.post(`${API_URL}/v1/shop/purchase`, { item: command.item }, {
...options, headers: { ...options.headers,
"idempotency-key": command.idempotencyKey },
}).then(read<PurchaseResult>),
};Do not scatter URLs, headers, untyped objects, or response parsing through UI components.
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRoot } from "@graaly/react";
import { events, PlayerJoinEvent, PlayerQuitEvent } from "graaly";
import { PlayerInterface } from "./components/player-interface";
import { ShopProvider } from "./state/shop-state";
const roots = new Map();
function renderPlayer(player, shopRequest = 0) {
const id = String(player.uniqueId);
const state = roots.get(id) ?? { root: createRoot(player), query: new QueryClient() };
roots.set(id, state);
state.root.render(
<QueryClientProvider client={state.query}>
<ShopProvider>
<PlayerInterface player={player} shopRequest={shopRequest} />
</ShopProvider>
</QueryClientProvider>
);
}
events.on(PlayerJoinEvent, ({ player }) => renderPlayer(player));
events.on(PlayerQuitEvent, ({ player }) => {
const id = String(player.uniqueId);
roots.get(id)?.root.unmount();
roots.get(id)?.query.clear();
roots.delete(id);
});
// main.tsx adapts the game lifecycle only.
// Hooks, components, API client, and reducer live in focused modules.The player clicks an <Item onClick>.
React receives a typed action with player ID, slot, click type, Shift, and right-click state.
await http.post(...) runs off the tick loop; its continuation returns on the safe server thread.
FastAPI authenticates the bearer actor, resolves live permissions, validates the payload, then SQLAlchemy commits.
The result updates React state; Graaly diffs inventory slots, lines, tab, and boss bar without reopening unchanged UI.
examples/ReactFastApi.jspluginThe checked-in example includes per-player TanStack Query caches, optimistic rollback, a React role editor, JWT identity exchange, live RBAC dependencies, async SQLAlchemy models, Alembic migrations, authenticated WebSocket, generated OpenAPI TypeScript models, JavaScript and Python permission clients, health/readiness probes, and automated security, transaction, migration, socket, renderer, and contract tests.
npm install react@19.2.8 @graaly/react graaly @tanstack/react-query@5.102.1
npm install --save-dev typescript esbuild @types/react
npm run build
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -e '.[test]'
GRAALY_DATABASE_URL=sqlite+aiosqlite:///./graaly-ui.db alembic upgrade head
GRAALY_API_KEY=service-secret GRAALY_JWT_SECRET=32-byte-minimum-secret uvicorn app.main:app --host 127.0.0.1 --port 8000127.0.0.1, keep service and JWT secrets in the environment, set request timeouts, validate prices and rewards in Python, never trust an item name sent by the UI, and keep an offline fallback. Run Alembic before startup, close WebSockets in Effect cleanup, make writes idempotent, resolve permissions in FastAPI, and never grant a reward from optimistic state. Graaly cancels outstanding requests, aborts sockets, and unmounts UI when the plugin disables.These are 36 long-form lessons, not a list of snippets. Edit and execute real TypeScript React against a Minecraft surface simulator, trace the FastAPI request lifecycle, and then run the same architecture in the checked-in plugin and backend tests.
Start with a component and finish with a transactional, realtime party shop. Every lesson connects the language model to Minecraft, contrasts it with the web, and includes editable code backed by runnable tests.
components, JSX, props, children, composition, conditional rendering, lists, keys, events, useState, snapshots, immutability and batching
lifting state, single source of truth, controlled and uncontrolled components, preservation, reset, identity, useReducer, normalization and derived state
render and commit phases, reconciliation, component and render trees, pure rendering, referential equality, position and key
useEffect, dependencies, cleanup, external synchronization, race conditions, event/effect separation, useRef, ref props and imperative handles
Context, Providers, custom Hooks, memoization, Profiler, Suspense, lazy, transitions, deferred values, optimistic state, Actions, Error Boundaries and portals
feature architecture, container/presentational split, compound and headless components, CSR, SSR, hydration, streaming, Server Components and Minecraft applicability
applications, path operations, path/query/body/header/cookie/form/file input, parsing, conversion, validation, nested request models and validation errors
request/output schemas, fields, model validation, nested schemas, serialization, response validation, filtering, status, headers, cookies and custom responses
Depends, sub-dependencies, trees, caching, yield cleanup, parameterized and class dependencies, APIRouter composition, prefixes, tags and sub-applications
exception handlers, middleware ordering, lifespan, resource cleanup, background tasks, security dependencies, OAuth2 concepts, scopes and OpenAPI customization
sync/async endpoints and dependencies, threadpool behavior, WebSocket lifecycle, SSE, JSON Lines, StreamingResponse and async generators
TestClient, dependency overrides, lifespan, WebSockets, exceptions, application state, SQLAlchemy transactions and contract drift tests
TanStack Query, cache keys, invalidation, cancellation, optimistic mutations, rollback, state machines and explicit impossible states
ESM, closures, prototypes, promises and microtasks, async iterators, generators, AbortController, Error cause, WeakMap, JSDoc @ts-check, unknown, branded types, satisfies, mapped, conditional and template literal types, Result and project references
Protocol, generics, dataclasses, structural matching, async context managers, TaskGroup, cancellation, ContextVar, pytest fixtures and dependency injection
JWT identity exchange, live RBAC, service tokens, rate limiting, settings, Alembic migrations, observability, correlation IDs, reconnect, multi-server coordination, health, readiness and graceful shutdown
A component is a pure recipe. React calls the recipe; Graaly commits its returned tree to inventory, chat, scoreboard, boss bar, or tab surfaces.
JSX does not create DOM nodes. The compiler turns it into React element descriptions. That is why the same component model can target a game UI: the Graaly renderer interprets Inventory, Item, Scoreboard and Message host elements.
Props are inputs for one render. children is only another prop, but it makes composition natural: a reusable shell can own layout while a feature supplies the items. Keep the component free of server mutation during render.
The preview renders an inventory surface. On the server, the identical tree becomes a real player inventory and the Item callback receives a typed game action.
Browser React commits div and button nodes. Graaly commits protocol-native UI surfaces; JSX, props, children, identity, state, Context and Effects remain React.
Use composition when a component owns presentation. Pass a callback only for behavior the parent genuinely owns; do not turn every label into global state.
POST /v1/shop/purchaseThe board renderer is still experimental and is not part of Graaly's stable public contract yet. Documentation and examples will return when rendering, input, scrolling, and lifecycle behavior are ready to support.
Status: planned, not available in the current release.
PacketEvents stays a separate 2.13.0 plugin. Start with practical receive, send, wrapper, cancellation, player-data, and threading guides; then search every client packet, server packet, wrapper, and supporting type below, including ClientPacket.CHAT_MESSAGE, ServerPacket.UPDATE_HEALTH, and WrapperPlayServerUpdateHealth.
8 practical guides
Use it during startup when packet features are optional or before registering packet-specific behavior.
Reports the runtime integration state without exposing PacketEvents' static singleton to script code.
Nothing
available boolean and version string or null/None
packets.availablepackets.versionPacketEvents#getAPI lifecycle and version accessimport { info, packets, warn } from "graaly";
if (!packets.available) {
warn("PacketEvents is not installed; packet features are disabled");
} else {
info(`PacketEvents ${packets.version}`);
}info(value) writes an INFO line to this plugin's server logger; warn(value) writes a warning. They do not send text to players.Constants explain what travels over the protocol and generate the correct receive or send listener. Wrappers show every JS, TypeScript, Python, and Java signature.
Search the full public surface: 1421 Graaly API symbols, 289 packet wrappers,533 supporting packet types, and 288 packet constants. TypeScript, Python, and Java signatures are shown side by side, including inherited members and overloads.
Native property rule: JavaScript and TypeScript use properties such as player.flying and player.allowFlight = true; Python uses player.flying and player.allow_flight = True. JavaBean instance accessors such as isFlying() and setFlying(...) are intentionally not exposed.
Graaly checks every compiled public type against the generated JavaScript, TypeScript, Python, and documentation catalogs. Three checked-in conformance plugins define 31 behavioral cases and compile against that same contract. Dedicated JavaScript and Python probes then run on every supplied real server from 1.7.10 through 26.2; focused Java, React, and FastAPI suites cover their own isolated boundaries.
Every compiled public top-level and nested type is present. Generated member signatures must have Java, JS, TS, and Python forms, with zero unresolved SDK types.
All 67 supplied releases load JS and Python, schedule work, dispatch commands, reload, spawn and remove an entity, adapt canonical constants, and report unavailable mechanics natively.
Join, move, chat, quit, entity effects, player-bound attributes, and packet traffic stay explicitly labeled as context-dependent; they are not presented as universal passes without the required player or PacketEvents state.
The real-server harness checks load, enable, task execution, command dispatch, repeated reload, disable, and clean shutdown in isolated directories.
Keep live world mutations on the main thread, and only install code you trust. Script plugins have the same power as JAR plugins.
Players, inventories, worlds, blocks, entities, commands, and most live server state.
Database, HTTP, file parsing, and packet inspection. Use tasks.run in JS/TS. Python coroutines run on Graaly's main loop; await tasks.to_thread(...) resumes there automatically.
Check the runtime, bundle entry point, dependency order, and thread before debugging plugin logic.
Use Oracle GraalVM 25 Innovation 2 (Graal 25.2.4 on JDK 25.0.4). A plain 25.0.4 CPU build has a different JVMCI compiler and falls back to interpreted Polyglot execution.
The entry in plugin.yml must point to compiled ESM, normally dist/main.mjs. Run the bundle build before starting the server.
No. This is a real React custom renderer whose host elements are Message, Inventory, Item, Scoreboard, Line, BossBar, and Tab. React hooks work normally; browser DOM elements belong only to a website board.
No. Graaly uses Java's asynchronous HTTP client, enforces a timeout, and never blocks the server tick. Catch the rejected promise or Python exception and render an offline fallback.
No. Keep a thin Graaly adapter in the game process. It receives events, calls FastAPI with serializable data, then safely applies the result to live players and React roots.
Install PacketEvents 2.13.0 separately and add depend: [packetevents] to the script bundle. Graaly does not redistribute it.
PacketEvents must receive cancellation and wrapper changes before its network callback returns. Graaly rejects async functions and generators at registration. A hidden Promise, coroutine, iterator, or any non-void return quarantines that listener after one diagnostic, so repeated packets cannot amplify the same stack trace. Keep the listener synchronous; use tasks.run(() => ...) in JS/TS, or capture packet data and call tasks.create_task(coroutine) in Python without returning it.
The database was temporarily unavailable or locked. Graaly's example maps SQLAlchemy operational failures to 503 Service Unavailable with Retry-After: 1. Retry a read with backoff; retry a purchase only with its original Idempotency-Key.
Use standard asyncio for sleep, gather, timeout, task creation, and cancellation. Graaly only adds tasks.create_task for synchronous entry points, sleep_ticks, to_thread, and is_main_thread.
No plugin-owned state survives: tasks, listeners, packet bindings, React roots, HTTP requests, Python modules, globals, and the event loop are cleared. JavaScript closes its Context. GraalPy keeps one isolated interpreter Context per unchanged bundle, then rebuilds its graaly facade and re-executes the current source. This avoids materializing another full Python runtime on every /reload without preserving stale plugin code or data.
Prefer pure-Python packages. Native CPython wheels may require explicit GraalPy compatibility and are not automatically portable.
Install Chrome or Chromium, verify the configured executable path, and keep the compiled site under the GraalyBoard data directory. Remote assets require an explicit HTTPS host allowlist.
Click the input or textarea, then send one chat message. GraalyBoard cancels that message before public chat and types it into the focused DOM element. Enter !cancel to abort.
Look at the board and use the mouse wheel. GraalyBoard scrolls that viewer's DOM and restores the selected hotbar slot. Tune distance with website-browser.scroll-pixels-per-step.