Skip to content
All projects
Basalt Server

The Minecraft server, rebuilt.

A complete reimplementation of the Minecraft server, written entirely in C++23 and shipped as a native binary for each platform. It aims to play exactly like vanilla, and it has no garbage collector to pause the tick.

Early development

Basalt Server is young and not finished. Expect unexpected behaviour, missing features and changes between releases, and keep backups of anything you care about.

Download

One binary, nothing else to install. Download it, make it executable (chmod +x on Linux and macOS) and run it: the server asks you to accept the EULA and writes its own configuration next to itself on first start.

Download for macOS Apple silicon, version 26.3.2

Detected from your browser and it can be wrong. Pick another platform below if so.

Download for macOS Intel, version 26.3.2

Detected from your browser and it can be wrong. Pick another platform below if so.

Download for Linux x86_64, version 26.3.2

Detected from your browser and it can be wrong. Pick another platform below if so.

Download for Linux aarch64, version 26.3.2

Detected from your browser and it can be wrong. Pick another platform below if so.

Download for Windows x86_64, version 26.3.2

Detected from your browser and it can be wrong. Pick another platform below if so.

Pterodactyl egg

Install Basalt Server from a Pterodactyl panel. Updated 1 Nov 2026.

Download egg

Version 26.3.2. All releases. Each file is the executable itself, with a SHA256SUMS file beside them. A nightly build follows every change, and the releases page has the rest.

Parity is the requirement

Basalt Server is built to behave exactly like the official one with no plugins: same seed, same world, same drops, same damage, same redstone, same mobs, bugs included. Parity is the acceptance criterion for every system, and it is not finished: some mechanics are still missing.

Anything that makes the server behave differently from vanilla is opt-in and labelled as such in the generated configuration comments. The default path is always the vanilla one.

Every client, one server

Basalt tracks the newest Minecraft release rather than pinning a version. The core speaks one protocol, the current one, and every other client is normalised before it reaches it. Gameplay code never learns that older clients exist.

  • Current Java Edition Direct, no translation
  • Older Java Edition Translated at the edge
  • Bedrock Edition Translated at the edge
Language
C++23
Minecraft
26.3 native, older versions supported
Clients
Java Edition and Bedrock Edition
Distribution
Native binaries per platform
World storage
Linear regions; Anvil regions read and converted
Plugin API
Lua, .bspkg packages
Bukkit plugins
Optional, through Basalt Shim
Configuration
config/, one TOML file per domain
License
GPL-3.0
Plugins

Lua, sandboxed, packaged

Plugins are written in Lua and packaged as .bspkg files, each with a manifest, code, resources and metadata. They run inside a sandbox with a instruction budget and a memory ceiling, so a broken plugin is stopped without taking the server down.

The Lua runtime is the official Lua 5.5.1, built from source into the binary, so it is the same runtime everywhere Basalt runs with no Lua to install alongside it and no per-platform build of your plugin.

hello.lua
basalt.on("player_join", function(event)
    event.player:send(
        "<gradient:aqua:green>Welcome!</gradient>"
    )
end)

Text is authored in MiniMessage and rendered through the component model, never through legacy colour codes.

Bukkit

Keep your plugins ? Powered by Basalt Shim, a Java machine embedded in the server binary. Nothing Java starts until a jar is in plugins/, and no JDK is needed on the host.

Most of the ecosystem is Bukkit, Spigot and Paper plugins that will never be rewritten. Basalt runs them through Basalt Shim, without an external JVM. Compatibility is still growing: the documentation lists what is known to work.

There is no host Java path to configure and no JDK to install. With no jar in plugins/, no Java machine starts at all.

How it works
plugins/
plugins/
  my-lua-plugin/
  other.bspkg
  WorldEdit.jar
  • Maximum performance, no JVM at all Native Lua plugins
  • A plugin that already exists A Bukkit jar
  • Both at once They run side by side
Worlds

Multi-world from the first line

Not a plugin bolted onto a server that assumes one save. A world here is a dimension, so there is no folder with three dimensions buried inside it and no machinery pretending otherwise.

worlds.toml declares what to load, and only that. It never deletes anything.

  • Add a table and the world is created, or imported if its directory is already there.
  • Remove a table and it stops loading. The files stay put, and putting the table back loads it exactly as it was.
  • linked_to gives one server several complete sets of dimensions, each with its own Nether and End, portals included, with nothing rewriting portal travel.
  • Every world carries its own settings.toml inside its directory, so it is copied, moved and deleted along with the world it describes.
  • Anvil regions are read and written back as Linear. Migrating a world is copy the folder and start, never run a converter first.
config/worlds.toml
[worlds.world]
default = true
type = "normal"
dimension = "overworld"

[worlds.world_nether]
type = "normal"
dimension = "nether"
linked_to = "world"

[worlds.lobby]
type = "void"

[worlds.creative]
type = "flat"
seed = 42

Four worlds, one file. normal, flat and void for the hub that should not generate terrain it will never show.

Player data

Your worlds are not your player database

Vanilla keeps playerdata/<uuid>.dat inside the world folder, which is why resetting a map has always been more dangerous than it sounds. Basalt keeps players.db beside worlds/, never inside one.

Reset the map, copy a world to a test server, delete a dimension outright: nobody loses their group, their whitelist entry or anything else the server knows about them.

The rule that decides it

Configuration is what the operator writes. State is what the server writes.

  • Configuration lives in config/. State lives in players.db or in the world directory. Nothing sits in both.
  • There is no operator list in a configuration file. /op writes to the database, so no command ever rewrites a file you are editing.
  • groups.toml says what a group is. players.db says who is in it.
Server UI

Tab list, scoreboard and boss bar, out of the box

The things every server ends up installing three plugins for are files in config/, and they all speak the same language.

config/tab_list.toml
[profiles.default]
default = true
header = [
    "<bold><gradient:gray:white>BASALT</gradient></bold>",
    "<gray>%basalt_animation_website%</gray>",
]
footer = [
    "<gray>TPS <white>%basalt_server_tps%</white></gray>",
    "<gray>Players <white>%basalt_server_online%</white></gray>",
]

[profiles.staff]
priority = 10
when = ["%basalt_player_is_op% = true"]
header = ["<red>STAFF</red>"]

Profiles work the same way in every file that draws something: the highest priority whose conditions hold wins, and whatever it leaves out it takes from the default.

One placeholder vocabulary

The same %basalt_server_*% and %basalt_player_*% names work in the server list, the tab list, the scoreboard, the boss bar and the messages. Plugins register theirs into the same registry, so a plugin's placeholders work everywhere the built-in ones do, with nothing to install in between.

Animations, defined once

A list of frames in animations.toml becomes %basalt_animation_<name>% everywhere. The frame comes from the clock, not a per-player counter, so everyone online sees the same one and a player joining halfway through does not restart it.

It only sends what changed

A redraw becomes a packet when the text it produced is actually different. A static panel costs nothing however often it is drawn.

The defaults have an opinion. The tab list ships on, because a header and a footer are decoration and cannot get in the way. The scoreboard and the boss bar ship off, because they cover part of the screen and that is the operator's call to make, not a default to inherit. The tab list is also Java only, and the file says so: a Bedrock player is not missing something that failed to send, their game has nowhere to put it.

Permissions

Groups that do what they read like

Dotted names, a trailing * to match everything below, a leading - to deny. The most specific rule wins no matter what order they are written in, and anything nobody grants is denied.

There is no hidden ranking behind the groups and no permission a group gets for being called something. What one can do is exactly what is written, and groups inherit from each other to add or take away from what they got.

config/groups.toml
[groups.moderator]
inherits = "default"
permissions = [
    "basalt.command.kick",
    "basalt.command.teleport",
    "-basalt.command.seed",
]
Commands

Rename it, retire it, reword it

One file decides what a command asks for, what it is called, whether it exists at all and what it answers.

config/commands.toml
[commands.teleport]
permission = "basalt.command.moderator"
aliases = ["tpa"]

[commands.kill]
enabled = false

[messages]
"commands.seed.success" = "<gray>Seed: %1%</gray>"
  • Point it at a permission. Aim a command at one your moderators already hold instead of granting a new one. commands.toml names the permission; groups.toml decides who has it, and the two read in one direction only.
  • Turn it off. A disabled command is not registered, so it does not exist rather than refusing when someone runs it.
  • Give it other names. Aliases on anything, including commands that already shipped with their own.
  • Rewrite what it says. Replace any answer the server sends, keyed by the translation key it normally uses, with %1% and %2% for the values it was going to fill in.

Worth knowing before you reword anything. By default the server sends the translation key and every client renders it in its own language. Override one and it becomes literal text, the same for everyone, in whatever language you wrote it. That is the trade, and it is why there are no per-language tables: a server is normally aimed at one community rather than being multilingual.

Configuration

One file per domain, not one file for everything

config/
  server.toml        network, logging, performance
  worlds.toml        which worlds load, and how
  groups.toml        permission groups
  commands.toml      permissions, aliases, wording
  server_list.toml   how the server looks in the list
  tab_list.toml      the panel behind Tab
  scoreboard.toml    the panel down the side
  boss_bar.toml      the bar across the top
  animations.toml    text that moves, shared by the rest

Generated with comments and vanilla defaults on first start. A server.properties found on first start is imported once, so moving an existing server across is a copy.

Not a port

Nothing here is inherited by accident

Parity governs how the game behaves. It does not govern the shape of the software around it, and Basalt carries none of the fifteen years of accumulated decisions that a fork inherits whether it wants them or not.

The slot count ships at 80 rather than 20, because 20 is a number from 2010 and nothing in the server cares. What the list advertises is a separate setting, since a profile can show whatever it likes without changing who actually gets in. A world is a dimension. Player state is not filed under a map. None of these are fixes applied on top of something older: there was nothing older.

Game assets

Nothing proprietary ships with it

Basalt Server does not contain or distribute any proprietary Minecraft content: no structures, no loot tables, no datapacks, no textures, no jars.

Once you accept the Minecraft EULA, the server downloads the official server archive from Mojang, verifies its SHA-1 and unpacks only the data it needs into a local cache. That happens once per version; later starts touch neither the network nor the archive.

Ready when you are.

One binary, no runtime to install beside it, and a config folder that explains itself on the first start.