Documentation
Everything about running KodaHosting, extending it and making it your own. 69 categories. Parts of it were written with AI assistance, see the notice at the end. Last updated October 2026.
What KodaHosting is
KodaHosting turns a phone into a real Minecraft server. Not a client and not a remote control for somebody else's machine: the server process runs on the device, the world files sit in the device storage, and the app is the control panel around it. That one decision explains almost every design choice in this codebase, including the odd ones.
Three planes do the work, and it helps to keep them apart in your head.
- The device plane is the app itself. It downloads server jars and Java runtimes, starts and stops the process, owns the console, browses and edits files, makes backups and applies every change. If something mutates a server, it happens here.
- The connectivity plane is the tunnel. A bundled reverse proxy client forwards the Minecraft ports and the web dashboard through a public server, so friends can join without your home address ever leaving the router.
- The control plane is Supabase. It knows which devices belong to which user, which servers exist, which ports are taken, which releases are published and which extensions are approved. Every write goes through a function that checks a per device token.
A fourth piece is easy to miss: KodaDash, a Java plugin that runs inside the Minecraft server and serves a web dashboard on port 7867. The app writes small JSON files into the server folder for stats, backups and schedules, and KodaDash picks them up, which is why the dashboard still shows sensible numbers while the phone sleeps.
Your own instance
The public repository is a complete snapshot of the source. What it cannot contain is the instance: the addresses, keys and accounts that belong to whoever runs it, plus the admin tooling that holds the service role key. Here is where the line is drawn.
| Missing on purpose | Why it matters |
|---|---|
secrets_local.h | the Supabase URL, the public key, the AI key and the tunnel host that get compiled into the app |
| The Supabase project | its rows, storage objects, auth users and the edge function secrets |
| Tunnel and DNS credentials | the reverse proxy token and the DNS provider keys for custom domains |
| Signing material | the release keystore, its passwords and the Play credentials |
| The admin tools | the web panel and the Kotlin admin app |
One exception is on purpose. The imprint and the privacy policy name a real operator with a real address, because German law requires it, and a service that hides who runs it is not trustworthy anyway.
Running your own
- Clone the repository and build it once with JDK 21.
- Create a Supabase project and apply the SQL files from
supabase/in order, migrations first, then security fixes, then releases and extensions. Each file ends with a statement that reloads the API schema. - Create the storage buckets your features need:
artifacts,release_images,support_attachments,extension_artifacts,extension_icons. - Deploy the edge functions and give them their secrets.
get-tunnel-configis the one you cannot skip, the app asks it for its tunnel credentials. - Run an frps server, or use an existing service, and put its address plus your Supabase URL and public key into the native header file.
- Build a signed release and install it. The first screen registers a device and creates the row everything else hangs off.
- Optional: the Discord bot with your own token, and the admin panel if you prefer a browser over the admin app.
Building the app
One Gradle module plus a small native part that boots the embedded JVM. The first build takes a few minutes, later ones are quick.
export JAVA_HOME=/path/to/jdk-21 # newer JDKs break the Gradle version in use
./gradlew :app:assembleDebug # debug apk
./gradlew :app:assembleRelease -PKEYSTORE_PW=... -PKEY_PW=...
adb install -r app/build/outputs/apk/release/app-release.apk
./gradlew :app:testDebugUnitTest # unit tests, finishes in seconds
The first trap: a debug build and a release build cannot replace each other, because their signatures differ. Decide early which one your test device carries, otherwise you will uninstall the app and lose its data on the way.
The second trap: release builds strip every log call. Logcat stays silent in a release build, for your code and for the framework wrappers alike, so show diagnostics in the interface instead of trusting a log line. The app has its own debug log screen for exactly this reason.
Relevant devices are arm64 and run Android 11 or newer. The APK bundles only arm64 libraries, so an x86 emulator will install nothing useful.
All screens at a glance
The app has one main screen, one large per server screen, and a set of dialogs and helpers around them. Knowing this map makes the rest of the documentation easier to follow.
| Screen | What it is for |
|---|---|
| Main | your servers as cards, quick stats, and the ways into settings, support and this documentation |
| Create server | a step by step setup: name, type, version, memory, world, join address, optional modpack |
| Server detail | five tabs per server: dashboard, console, files, mods and plugins, settings |
| Settings | appearance, account, support, legal, developer options and the extension hub |
| Extensions | install, enable and configure extensions |
| Themes | the app colours plus every theme contributed by an extension |
| Support | tickets, bug reports and the personal chat |
| Onboarding | the first start: language, terms, account, background permissions, nickname |
Your servers, the home screen
The home screen lists every server as a card with its name, type, version and live state. A tap opens the server screen, a long press gives the quick actions that matter in the moment, and the cards carry the numbers you check most often: players, memory and uptime.
Servers that are hibernated appear greyed out with a wake button. Servers that are starting or stopping show the animated state instead of a button, so you can see that something is happening without opening the console.
Above the list you find the entry points to settings, support, Discord and the debug log, plus the button that starts a new server. The stats card at the top summarises how many servers exist, how many are online and how much memory they use together.
Creating a server
The wizard is deliberately linear, because every step has consequences that are hard to undo later. Name and join address first, then the server software, then the Minecraft version, then memory, then the world.
- Server type: Paper, Purpur, Folia and Vanilla for plugins, Fabric, Forge and NeoForge for mods, and PumpkinMC, the native server that needs no Java at all.
- Minecraft version: only versions the chosen software actually publishes, with short notes on mod and plugin availability.
- Memory: the wizard shows what the device can spare while other servers keep running.
- World: type, seed and a few presets, including a flat world for building tests.
- Setup mode: the Koda design installs a tuned configuration with sensible gameplay defaults, or you start from a clean vanilla setup.
After the wizard the server is created locally and registered in the cloud, and the first start downloads the server jar and, if needed, the matching Java runtime. Both downloads show progress in the console.
The server dashboard tab
The dashboard is the tab you land on, and it is built to answer three questions: is the server healthy, who is playing, and what can I do right now.
Status and address come first, with the join address in a copyable form and a button to change it. The player panel lists who is online, with avatars and one tap into the full player sheet for stats, inventory and moderation actions. Memory and CPU usage update live, and the quick action row starts, stops and restarts the server with a confirmation countdown when players are online.
When a server crashes, the dashboard is also where the crash report appears: a classified reason, the suggested fix, and the last log lines. Applying a suggested fix is a single tap and never guesses, it changes one known setting and tells you what it changed.
The console
The console streams the server output live, stripped of the colour codes that Minecraft servers write, with the levels highlighted so warnings and errors stand out. Auto scroll follows the newest line until you scroll up, then it stays where you are, which is the behaviour you want while reading a stack trace.
Above the input field sits a row of quick commands, and extensions can add their own chips to it. Commands you type are sent to the server process directly, so anything the server understands works here, including plugin commands.
- Copy puts the visible log on the clipboard, handy for a support ticket.
- Log opens the full verbose log of the app itself, useful when a download or a start fails.
- AI crash analysis offers to read a crash with you, and asks for consent before anything leaves the device.
Files: browse, edit, unpack, export
The file tab is a full manager for the server folder. Tap a folder to enter it, tap a file to open the editor, long press an entry for the action sheet with rename, copy, cut, delete and share.
- Import brings files in from anywhere the phone can read, including other apps and cloud providers.
- Unpack here extracts ZIP and RAR archives in place. Password protected RAR files are reported instead of failing silently.
- Export writes the whole folder as a ZIP into Downloads, without a wrapper folder so it can be reimported directly.
- NBT viewer opens player data, level data and other binary NBT files as a readable tree, read only by design.
The editor is for text files up to a megabyte, with syntax friendly handling for properties, JSON and YAML, and a save button that warns when the server is running because some files are only read at start.
Mods and plugins
Mods and plugins are managed in their own tab, and the install side is a Modrinth browser: search, filter by the server type and version, install with one tap, update with one tap.
The installed list shows every jar with its metadata read from the file itself: name, version, authors and icon, with a Modrinth lookup as a fallback because many server jars ship no image. A switch enables and disables a jar by renaming it, and the trash keeps removed files for a while instead of deleting them outright.
Starting, stopping, restarting
Starting a server is one tap, and the app orchestrates a lot behind that tap: it checks memory and network budget, prepares the runtime, writes generated configs, starts the process, and waits for the startup line before reporting the server as online.
Stopping asks the server politely first, sends the save command, and only kills the process when it does not react. The force kill option exists and warns that world data may be lost, which is why it sits behind a confirmation.
Restarts reuse the stop path, so players do not lose progress. When people are online, the server announces the restart in chat with a countdown.
Changing the Minecraft version
Version changes live behind a wheel on the dashboard and are treated as a structural operation, not a setting. The list only shows versions the current server software really publishes, and the switch takes a backup first.
- Upgrades ask for a backup but let you continue without one.
- Downgrades require one, because worlds and chunks are not backwards compatible.
- The old jar is kept until the new one started successfully.
Plugins and mods are not migrated automatically. After a version change the app tells you to check the mods tab, and Modrinth update checks usually find matching builds.
Backups and restore
Backups are ZIP archives of the whole server folder, stored beside the app, and they are the reason a bad afternoon stays an afternoon. You can create one manually, or let the app keep a schedule: on stop, daily, or both.
- Keep count decides how many copies survive the rotation.
- Restore replaces the server folder with the chosen state and keeps the current files until the restore finishes.
- Size and age are shown per backup, so cleaning up is a decision and not a guess.
Automations and schedules
The automation sheet plans recurring work: restart, stop, save the world, announce something or run a command. Jobs can run on an interval or daily at a fixed time, and they can wait until nobody is online.
A nightly restart with an announcement a few minutes ahead is the usual first job. The app notes in chat before an empty only restart, so players are never surprised, and the KodaDash dashboard shows the same job list because the app mirrors it into the server folder.
Players, stats and moderation
Player management covers both worlds: the live roster of who is connected, and the history of everyone who ever joined. Tapping a player opens a sheet with their skin, playtime, statistics and inventory rendered from the actual save data.
- Live actions: kick, ban, pardon, op and deop, message, gamemode, teleport, experience.
- Care actions: heal, feed, saturate, and a full inventory wipe that asks twice.
- Inventory can be rearranged by dragging items between slots, which writes straight into the player save.
- Whitelist switches per player and for the server as a whole, with the same rules the server uses.
The MOTD editor
The MOTD is the two line text players see in their server list, and the editor treats it like the formatting puzzle it is. Colours, bold, italic, underline, obfuscated, reset, plus hex colours in all three notations Minecraft accepts, are inserted as proper codes while you type.
Every code button inserts into the field and keeps your cursor in the right place, and a live preview shows how the text will look. If you prefer raw editing, the plain properties view is still one tab away.
Resource packs
A resource pack is served by the server itself, and the app handles the whole chain: pick a ZIP, upload it into the server folder, set the pack URL and the SHA-1 checksum in the properties, and switch the pack requirement on or off.
KodaDash serves the file over the tunnel so players can download it through the join address, which means no external hosting is needed. The dashboard shows whether a pack is active and how large it is.
World settings: type, seed, gameplay
World type, seed, gamemode, difficulty, view and simulation distance, spawn protection and hardcore mode are all edited in the settings tab and written into server.properties. The app reads the file back afterwards and compares it to what it wrote, so manual edits are never overwritten silently.
If you change a value by hand while the server is stopped, the app notices the drift on the next visit and asks which value should win. That is the reason the file and the interface never disagree, a problem that otherwise shows up as a server that ignores its own settings.
Memory settings
Each server gets its own heap size, and the app guards the total: Android keeps a reserve for itself, and starting a second server can only use what is left. The RAM slider shows what fits right now and warns when a value would push the device into swapping.
Changing memory requires a restart to take effect, because it becomes a JVM start argument. The app uses it to power the crash advice as well: an out of memory crash arrives with a suggested value that fits the device, not an arbitrary number.
Java runtimes
Minecraft servers need a Java runtime per version, and the app bundles none of them by default. Instead it downloads the matching runtime when a server needs it, from JDK 25 down to 8 and 17, and remembers the choice per server.
- Automatic picks the runtime that matches the server version and software.
- Manual lets you pin a version, for example a mod that insists on Java 17.
- Fallback starts the server anyway when a runtime is broken, and tells you what it did and why.
The download progress is visible in the console tab, and runtimes are shared between servers, so the second server of the same version starts much faster.
Hibernate and wake
Hibernation takes a server off the air without deleting it: the app stops the process, removes the join address from the network, packs the folder, and leaves a marker behind so the dashboard shows a sleeping server instead of a broken one.
Waking it restores the folder, requests a fresh join address, and starts the process. The point of the feature is the phone: a hibernated server uses no memory, no battery and no tunnel, which matters when you host more than one world.
MariaDB and Redis
For plugins that need a database, the app can run MariaDB or Redis next to the server, on the phone, with credentials you choose in the wizard. Both run as native processes from the same runtime set that runs the servers, so there is nothing to install separately.
The connection details are shown in the database card, including port, user and a generated password, and the app keeps the data directory beside the server folder so a backup covers both. The server type Database in the main list is exactly this: a database instance without a Minecraft server around it.
Tunnel and join addresses
Players cannot reach a phone on a home network directly, so the app runs a reverse proxy client that forwards the Minecraft ports and the dashboard through a public server. The address players use is the join address shown on the dashboard, and it changes with the tunnel, not with your home connection.
The tunnel credentials are not stored in the app package. The app asks the backend for them on demand, caches them for a few hours, and refreshes them in the background, which is why an expired tunnel heals itself after a while instead of needing a reinstall.
- Join address is created automatically and can be changed once per day.
- Both Java and Bedrock ports are forwarded when Bedrock support is on.
- The dashboard travels through the same tunnel on its own port band.
Custom domains
If you own a domain, the wizard links it to the server with an SRV record: it shows the exact record values, copies them to the clipboard, and verifies through public DNS until the record is live. No DNS knowledge is required, but the wizard explains what it does at each step.
Because DNS changes take time, the wizard can finish later: the server stays reachable on the standard address until the record resolves. The same card can unlink the domain and return to the default address.
Ports and network
Each server gets its own port, and the app manages both halves: the local port the server listens on, and the remote port the tunnel publishes. KodaDash runs on a separate port from a reserved band, so it never collides with a Minecraft server.
The port list on the dashboard shows what is in use and what is free. When a port is taken by another app on the phone, the server start fails with a clear message and a suggestion instead of a generic crash.
Network budgets and the network alarm
Hosting from a phone means hosting on a connection that other people also use. The app therefore tracks how much bandwidth each server consumes and can hold a server back when a rule is set.
- Budget rules define a speed limit and a split between servers, so one busy world cannot starve the others.
- The network alarm warns when the shared tunnel server is under pressure, before players notice lag.
- Protection mode stops servers when the network is overloaded and tells you why, instead of letting everyone lag.
The KodaDash web dashboard
KodaDash is a small Java plugin that runs inside the Minecraft server and serves a web interface on its own port, reachable through the tunnel. It is the answer to a simple question: what if I am not holding the phone?
| Tab | What it does |
|---|---|
| Overview | status, players, memory, TPS history and the device the server runs on |
| Console | live console with streaming, history and a level filter |
| Players | roster with inventories, statistics and moderation actions |
| Files | browse and edit server files in the browser |
| Plugins | installed jars, enable and disable, Modrinth search |
| Settings | server.properties with comments preserved |
The dashboard has its own token and optional password, and the backups tab is deliberately read only: a stolen dashboard token should not be able to destroy the only copy of a world.
Bedrock crossplay
With Geyser and Floodgate installed, Bedrock players can join a Java server. The app installs both plugins, writes their configuration from templates that already match the server settings, and opens the extra port the Bedrock protocol needs.
PumpkinMC, the native server, has Bedrock support built in, so on that server the switch is simply part of the server itself. Either way the dashboard shows both addresses: the Java one and the Bedrock one with its port.
Voice chat
For Simple Voice Chat the app installs the plugin or mod matching the server type, opens the extra UDP port through the tunnel, and writes the voice chat configuration with the correct port and address. The result is voice that works for Java and Bedrock players without a separate server.
Your account
An account is optional, and the app works fully without one. With an account you get your servers on the website, the ability to recover them on a new device, and the family features.
Signing in happens with email and password or with Google, and the account links to the device, not the other way around: the device token stays the credential that authorises every write, and it never leaves the phone.
Linking a Minecraft account
Linking proves that a Minecraft player belongs to you, which is what the lobby and the /join command use. The app generates a short code, you join the lobby server and type /link with that code, and the link appears in your account card.
One Minecraft account belongs to one device at a time. Moving it to another device is a deliberate action, and the old device loses the link when the new one takes it, which keeps shared accounts from drifting apart.
Your nickname
A nickname is what friends see in the shared server list and in the lobby, instead of a device identifier. The app asks once after the first start and then about once a day when you open a screen that shows other people, and it can be changed in the settings at any time.
Moving servers to a new device
Transfer takes a server from one phone to another over the local network, encrypted end to end. The old device packs and serves the files, the new device downloads and adopts them, and the join addresses follow the server.
- Pairing works by picking the target device or by an eight character code, and the receiving side always confirms with a dialog that names the sender.
- Encryption uses a key that only exists during the transfer window, so the LAN traffic is unreadable even on a shared network.
- Cleanup happens after the receiving side confirms, never before, so an interrupted transfer leaves the old copy intact.
Family accounts and permissions
One account can hold several servers, and other people can be linked to them with individual permissions: who may start, stop, hibernate, delete, manage players, install plugins or use the console. The permission dialog lists them as switches, and a main account keeps full control.
Everyone signs in with their own credentials, so the permissions apply to people, not to devices. Revoking access is one switch and takes effect on the next action.
KodaCluster
Two phones can be joined with a USB cable into a small fleet: one device is the master and shows a dashboard of both machines, and can start and stop servers on the other device. The link speaks a small JSON protocol over USB, so it works without a network at all.
Tickets and bug reports
Support happens inside the app: a ticket with a subject and a message, attachments if you have them, and a conversation thread that shows replies. Bug reports and server reports are the same mechanism with a shorter form, so nothing gets lost between a chat message and a real ticket.
You need an account to open a ticket, because the reply has to reach you. The ticket list shows the state of everything you sent, and the conversation keeps its history.
The personal chat
Beyond tickets there is a direct line to the developer, encrypted end to end. The keys are generated on the device, the private key never leaves it, and messages are encrypted with ECDH and AES-GCM before they are stored.
On the other side only the developer can decrypt, using a key that lives in a separate admin app behind a PIN. That is the entire point of the feature: honest words stay between two people, and nobody in between can read them, including the server that stores them.
The help screen
When something in a session suggests that a person is not doing well, the app shows a quiet screen that exists for one purpose: to say that someone cares and to offer real ways to get help, including hotlines, a chat service for younger people and the personal chat.
It never blocks, never reports anything, never changes a server, and disappearing from it needs a short countdown. The screen is part of the app because the people who use it are people, and the project is built by one.
Bans and enforcement
A ban is the last resort, and the app treats it that way. Server names are checked before they are created, reports are reviewed by a human, and a banned server shows a screen that explains the reason and offers a backup download instead of a dead end.
Device bans address abuse that survives reinstalls. They are rare, and the code that triggers them is deliberately narrow: file tampering with the app's own state and forged identities, nothing else.
What extensions are
Extensions let other people add screens, languages, themes, settings rows and server tabs to the app, without forking it and without waiting for a release. A package is a ZIP file with a manifest and, usually, a small web interface. Nothing native is ever loaded, which keeps the app compatible with Play policy and keeps a broken extension from taking the app down with it.
The examples are the fastest way in, and they are all in the repository:
- Koda Lab, the test bench that exercises every call with raw answers
- Live Log, a screen in about a hundred lines
- Polski, a real language pack
- Ocean, a theme plus interface tweaks
- The written reference, with the JSON schema beside it
Package format
my-extension.kodaext (a ZIP, files at the root)
manifest.json required
ui/index.html required when the extension has a screen
ui/app.js, ui/style.css your interface
lang/es.json language packs you contribute
automations.json automation rules
icon.png 128x128, shown in the hub
Here is a manifest that uses nearly everything. Read it as a menu, then delete the parts you do not need.
{
"schema": 1,
"id": "eu.example.mypack",
"name": "My Pack",
"version": "1.0.0",
"versionCode": 1,
"author": "Your name",
"description": "What it does, in one sentence.",
"license": "GPL-3.0-or-later",
"source_url": "https://github.com/you/mypack",
"minAppVersion": 8514,
"permissions": ["servers.read", "console.read", "storage", "http:discord.com"],
"ui": { "entry": "ui/index.html", "title": "My Pack" },
"contributes": {
"languages": [ { "code": "es", "name": "Espanol", "file": "lang/es.json" } ],
"themes": [ { "id": "ocean", "name": "Ocean", "color": "#006874" } ],
"settings": [ { "title": "My Pack", "subtitle": "Open the panel", "target": "overview" } ],
"server_tabs": [ { "id": "panel", "title": "Panel", "target": "server" } ],
"ui": { "hide": ["btn_github_main"], "console_chips": ["/tps", "time set day"] }
},
"automations": "automations.json"
}
| Field | Rule |
|---|---|
schema | must be 1 |
id | lowercase reverse domain, at least two segments, at most 64 characters |
name | at most 48 characters, shown in the hub |
version and versionCode | the code must grow with every update |
license | mandatory, the app shows it next to the extension |
source_url | recommended, shown as the source link |
minAppVersion | the app build the extension needs |
permissions | declared capabilities, enforced by the app |
contributes | everything the extension adds to the app itself |
The bridge API
Every page your extension shows runs in a sandbox that can only reach its own files. To talk to the app it uses one global object, window.koda, injected before your scripts run. Every call returns a promise and every answer is a JSON object. Errors arrive as an object with an error field instead of an exception.
const { servers } = await koda.servers.list();
const first = servers[0];
const { lines } = await koda.console.tail(first.id, 200);
await koda.storage.set('lastServer', first.id);
const saved = await koda.storage.get('lastServer'); // { value: '...' }
const theme = await koda.theme(); // colours, so your page can match the app
const { lang } = await koda.locale();
koda.ui.toast('Hello from my pack');
const { ok } = await koda.ui.confirm('Restart?', 'Players will be kicked.');
| Call | What it gives you |
|---|---|
koda.api, koda.version | the bridge version, to show a friendly message on older apps |
koda.servers.list(), get(id) | servers with a fixed field set: id, name, type, mcVersion, state, port, address, ramMB, maxPlayers |
koda.console.tail(id, lines) | the last console lines, up to a thousand |
koda.storage.get/set/remove/keys | your own key value store, one megabyte per extension |
koda.ui.toast, koda.ui.confirm | two helpers that use the app's own look |
koda.theme(), koda.locale() | colours and language, so your page fits in |
Permissions
Permissions are declared in the manifest and enforced by the app. If you did not ask for servers.read, the call comes back as an error.
| Permission | State |
|---|---|
storage, servers.read, console.read | live today |
console.send, servers.control | declared, next phase, behind in app confirmation |
players.read, players.actions, files.read, notifications | declared, next phase |
http:host | reserved for one specific web domain |
Language packs
// lang/es.json, the keys are the app's string resource names
{
"tab_dashboard": "Panel",
"jars_title": "MODS Y PLUGINS",
"sd_toast_log_copied": "Registro copiado",
"ext_ui_chips_count": "%1$d comandos extra en la consola."
}
Packs are partial by design. Whatever you leave out stays in the language the user picked, so a small pack is useful from the first day and can grow later. Keep placeholders such as %1$s exactly as they appear in the original string, because a missing or extra one crashes the app the moment that text is formatted.
python3 extensions/tools/check_language_pack.py path/to/lang/es.json
The checker compares your keys against the app's strings and reports the ones that do not exist, which would otherwise do nothing at all. Applying a pack happens in the extension's actions sheet and needs a restart, and the pack follows the extension: disabling it restores the previous language.
Themes, including full palettes
"themes": [
{ "id": "ocean", "name": "Ocean", "color": "#006874" },
{ "id": "midnight", "name": "Midnight", "color": "#00D9FF",
"colors": { "primary": "#00D9FF", "surface": "#07070F", "card": "#14141F",
"text": "#EDEDF5", "text_dim": "#8C8C9C", "error": "#FF4455" } }
]
The color field alone is a seed. The app runs it through its Material 3 engine and generates a whole palette from it, which already changes buttons, cards and highlights. With colors you declare the palette yourself: primary, surface, card, text, text_dim, outline, online and error override what the engine computed, and the app derives a readable contrast colour for text on top of your accents.
Themes live in two places, on purpose: in the extension's actions sheet, and in the app's own settings under Appearance, Themes. Both list every contributed theme with its own switch, and switching one on switches the previous one off.
| Where the app runs | What a theme changes |
|---|---|
| Material 3 mode | the whole interface, because the app resolves colour roles and styles screens with them |
| Classic design | accent, surfaces, cards, texts and outlines, mapped onto the colours the classic design paints with |
Settings entries
"settings": [
{ "title": "My Pack", "subtitle": "Everything in one screen", "target": "overview" }
]
These appear in the app's settings screen, right below the extension hub, each with a small info button that names the extension, its version, the author and the license. That button is the point: a user should never have to guess which rows come from the app and which come from a pack.
Tapping the row opens your screen, and target is appended to your page address as #target, so you can open the right tab of your own interface. Four entries per extension are allowed.
Server tabs
"server_tabs": [ { "id": "panel", "title": "Panel", "target": "server" } ]
This adds a tab to every server screen. Your page is loaded with the context attached, for example #server&server=the-server-id, so your interface knows which server it belongs to and can show something specific instead of a generic list. Reading the fragment is one line of JavaScript:
const parts = location.hash.replace('#', '').split('&');
const server = (parts.find(p => p.startsWith('server=')) || '').split('=')[1];
Interface tweaks
"ui": {
"hide": ["btn_github_main"],
"console_chips": ["/tps", "/list", "time set day"]
}
hide removes elements you do not want, addressed by the view ids used in the app's layouts. console_chips adds your own quick commands above the console. Both stay active while the extension is enabled, with no extra switch to hunt for, which is the trade off of these two: they are part of enabling the extension.
Automations
{
"schema": 1,
"automations": [
{ "id": "crash-watch", "name": "Crash warning",
"when": { "type": "log_match", "server": "*", "pattern": "(?i)(crash|exception)" },
"then": [ { "type": "notify", "title": "Crash on {server}", "text": "{line}" },
{ "type": "http", "method": "POST",
"url": "https://discord.com/api/webhooks/...", "body": { "content": "{line}" } } ] }
]
}
Rules have a trigger and a list of actions. Triggers are server state changes, log patterns and schedules. Actions are notifications, webhooks and console commands. Placeholders such as {server} and {line} are filled in when the rule runs.
The format is final and validated when the package is installed, and the engine that runs the rules ships in the next phase, so think of the file today as a precise description of what your extension will do on its own.
Packaging and testing
# validate the manifest and build the package
python3 extensions/tools/pack_extension.py extensions/examples/koda-lab
# check a language pack before you ship it
python3 extensions/tools/check_language_pack.py my/pack/lang/es.json
Both tools apply exactly the rules the app applies. An unknown view id, a missing language file, a malformed colour or a version number that does not grow will fail on your computer instead of on somebody's phone.
To try a package, sideload it: Settings, Extensions, plus, pick the file. Updates keep the extension's storage, removing it deletes the storage with it. The hub menu has a reload entry, so you can iterate without reinstalling.
The store and the API
The store lives at host.kodanetwork.eu/store and mirrors inside the app's extension hub under Discover. Submitting happens on the website, installing happens in the app, and both talk to the same public API.
Submitting
Sign in with the same account you use in the app, pick your .kodaext package, fill in the name, version, license and a source link, and submit. The form computes the SHA-256 of your file and the server repeats the check: it reads the manifest out of the package and refuses anything whose id, version code, license or permissions do not hold up. Nothing reaches the store before the review approves it, and the first approved version publishes the extension.
A submission is in one of five states: PENDING while it waits, APPROVED once it is live, REJECTED with a reason, DELISTED when it was taken down after the fact, and WITHDRAWN when you ended it yourself. The dashboard on the website lists your submissions with their state and the reason, so an ended extension is never a silent one.
New versions
A new version of a published extension goes through the same review. Until it is approved the store keeps serving the previous version, so a broken update can never take the published one down with it. Installing an update puts it over the existing copy and keeps its storage.
The API
Everything the website and the app do is reachable under https://api.kodanetwork.eu. The public routes need no key:
GET /v1/status live numbers (servers, players, store, tunnel server)
GET /v1/releases published app releases
GET /v1/servers the public server directory (name, status, players, MOTD, owner)
GET /v1/servers/{host}?ping=1 one server: owner, and a live minecraft ping (players, MOTD, favicon)
GET /v1/extensions the catalog, approved only, with the newest version
GET /v1/extensions?author=Name everything one publisher released
GET /v1/extensions/{id} one extension with all approved versions
GET /v1/extensions/{id}/download redirects to a 60 second signed url
The rest takes the Supabase session token as a bearer, the same login the dashboard uses:
GET /v1/me profile, nickname, avatar url, counters, 2FA state
PATCH /v1/me nickname and avatar
POST /v1/me/avatar upload target for a profile picture
GET /v1/me/devices linked devices with their telemetry and MC accounts
POST /v1/me/devices/{id}/unlink detach a device; the app keeps working standalone
GET /v1/me/extensions your submissions with their review state
GET /v1/me/tickets your support tickets
POST /v1/me/tickets/{id}/messages reply to a ticket from the browser
The server detail answers with a real minecraft server list ping when the server is reachable: live players, the MOTD and the icon the server itself advertises, all cached for a minute. The directory and the detail only ever show the owner's minecraft name, and only when that account linked one - never app uuids, tokens or hardware ids.
Two things stay out of the API on purpose. The personal support chat is end to end encrypted between the app and the admin app, so it only exists in the app. And packages are never served from a public url: a download goes through a signed link that expires after a minute, so a pending or rejected upload is not reachable by guessing paths.
Every route sits behind sliding-window rate limits (reads 120 per minute per address, writes 30, ninety per account, twenty for ticket replies and uploads), counted in the database so they hold across all edge instances. When a client hits a limit the api answers 429 with a Retry-After, and the website shows a "service is busy, please wait" screen with a countdown instead of errors.
Translations
All interface text lives in app/src/main/res/values/strings.xml, with translations beside it in values-de, values-zh-rCN and values-pl. Android falls back to the default for anything missing, so a partial translation is genuinely useful and can grow over time.
Adding a language means creating the matching folder, adding the language to the picker in the settings activity, and nothing else. Two tools keep translations honest:
python3 extensions/tools/check_translation.py app/src/main/res/values-pl/strings.xml
python3 extensions/tools/check_language_pack.py path/to/lang/es.json
Theming in code
The app is Material 2 at heart with an opt in Material 3 mode that generates a full colour scheme from a seed. In Material 3 mode the helper resolves the colour roles once and styles whole view trees with them, which is also where extension themes hook in. The palettes live in colors_m3.xml with the night variant beside it.
In the classic design the app paints with fixed colours, and extension palettes are mapped onto those values. If you want a colour decision that applies everywhere, the theme helper is the single place to change it, and the mapping tables in it are the list of colours you would have to touch otherwise.
Layout variants, and the trap that keeps biting
Central screens exist in more than one layout: a classic one, an optional Material 3 one, an optional sleek one, and for the server screen a terminal variant. The activity picks at runtime, which means a new view has to be added to every variant you actually use, or the code that looks it up has to cope with its absence.
We learned this the hard way. A settings card existed only in the classic layout, the settings screen crashed in Material 3 mode with a null pointer, and because that same screen is where the theme mode lives, there was no way back. The habit that prevents it is cheap:
View card = findViewById(R.id.card_my_feature);
if (card == null) return; // this layout variant may not have it
card.setOnClickListener(v -> openMyFeature());
Branding and assets
Icons, the K monogram and the brand colours live in branding/ and are generated by a script, so you change the source instead of twenty files. Fonts are referenced as font_koda for the interface and font_koda_mono for logs and code, and their licenses sit with all the others in the licenses folder.
Server software is not bundled in the app: jars and runtimes are downloaded on demand and verified, which keeps the package small and the licenses clean. The app ships only its own assets, the bundled server side plugins and the license texts.
The design rules
The rules that keep the app coherent are worth stating plainly, because breaking them is visible to anyone who uses the app for a day.
- Use the Koda palette instead of inventing greys. Orange marks actions, online marks health, red marks danger.
- Buttons keep a 12 dp corner radius, cards a 12 dp radius on a flat surface with no shadow.
- Every new string ships in English, German and Chinese at least, and now Polish as well.
- Functional sheets stay functional: controls and one result, no paragraphs of prose.
- Screens that belong to a flow copy the flow's layout instead of inventing a second look.
Adding a screen or a setting
New screens are activities plus layouts, wired with findViewById and listeners. There is no navigation library in the app, which makes each screen readable on its own and makes adding one a small, local change.
To add a settings entry, mirror an existing card in both settings layouts and wire it in the settings activity, or insert it at runtime the way the extension entries do. To add a server tab inside the app itself, extend the tab list and the panel logic, which is exactly what extension server tabs do programmatically, so they double as a reference implementation.
The database
The control plane runs on Supabase, and the schema is deliberately small and readable. Everything a client is allowed to do goes through a function, tables themselves are closed to the API.
| Table | What it holds |
|---|---|
koda_users | one row per device: app uuid, device token, linked Minecraft account, nickname, permissions, ban state |
koda_servers | one row per server: owner, type, version, port, join address, state, join address history |
koda_ports | the port pool, with allocation and release handled by edge functions |
releases | app releases with changelog, channel, banner image and a published flag |
app_settings | remote configuration: maintenance mode, minimum versions, feature flags |
support_tickets, support_ticket_messages | tickets and their conversation, including the encrypted personal chat |
extension_* | the extension registry: packages, versions, reports |
Functions follow one naming rule: anything a client may call starts with rpc_, internal helpers start with fn_. Read functions that replace a table are the norm, and every write verifies the device token inside the function rather than trusting the caller.
Edge functions
Small Deno functions handle the work that needs a secret or a decision the client should not make itself: allocating and freeing ports, checking whether a server name is free, creating and deleting DNS records, provisioning a Java runtime, and handing out the tunnel configuration after verifying a device token.
They are the only place where credentials for external services live, and each one validates its caller first. A typical request carries the app uuid and the device token, and the shared helper in supabase/functions/_shared does the verification in one place.
Inside KodaDash
KodaDash is a Java plugin with its own small HTTP server, built on the JDK's built in server so it has no dependencies beyond the plugin API. It registers one handler per area: console, players, files, settings, plugins, logs, backups, schedule, stats, efficiency and the server actions.
The console handler is the interesting one: it attaches a Log4j appender through a dynamic proxy, keeps a bounded per client queue with a writer thread, and streams events with a line index so a reconnecting browser resumes exactly where it stopped. That is why the dashboard console survives a phone going to sleep and back.
The dashboard never writes files the app owns. The app mirrors stats, backups and the schedule into the server folder, and KodaDash reads them, which keeps one writer per piece of state.
The Discord release bot
A small Node.js bot watches the releases table and announces anything that is published and not yet announced. It runs on the server as a service, reads with the service role key, and marks each release after posting, which makes it safe to restart at any time.
Each announcement carries the changelog and the release banner. The bot never writes releases itself, publishing stays a deliberate action in the admin panel.
The website
The website is a static site with a dashboard page that talks to the same Supabase project through the anon key and a real user login. It answers the questions a phone screen is bad at: comparing servers side by side, reading a changelog on a big screen, or checking whether a server is up while away from the app.
The legal texts and the third party license list are the same files the app ships, so there is one source of truth and one place to correct a typo. This documentation page is part of the same site and lives in the same repository as the code it describes.
The admin tools
Two tools exist for the operator, and both are private by design, because both hold the service role key.
- The web panel for publishing releases, uploading banners and reviewing the extension registry.
- The Android admin app for everything that should not wait for a browser: user and hardware bans, tickets, the email centre, DNS and server access.
Neither tool is part of the public repository. If you run your own instance, you either build equivalents or use the Supabase dashboard directly, which is enough for a small instance.
The security model
The app ships a public API key, and that is fine because the key alone authorises nothing. Every write carries the device uuid and a per device token, and the server side functions verify that pair before they touch a row.
- Device tokens are random, per installation, and rotate when a device is reset.
- Row level security is on everywhere, with no permissive policies on the tables clients use.
- Admin functions additionally check an allowlist, so holding the service key is not the same as being an admin.
- Anti tamper watches the app's own state and identity, and is deliberately narrow to avoid punishing honest users.
- Attack simulations in the repository try the classic moves with the public key: writing without a token, escalating to admin, spoofing a ticket, reading tables directly. They must all fail.
Working on the code
The project uses two repositories: a private development repository and a public mirror. The mirror is produced by a script that copies tracked files, excludes the local tooling, the IDE state and database backups, and scans the result for instance data before it pushes. A pattern that has never been public before aborts the push, which is the only reason a project this size can be published safely.
Branches are simple: work happens on a feature branch, releases are tagged, and the mirror receives a snapshot commit. When you fork, the same script is your safety net for anything you adapt.
The checks that run around a change
None of these is ceremony. Every check exists because the corresponding mistake reached a device at least once.
| Check | What it protects |
|---|---|
./gradlew :app:testDebugUnitTest | extension manifest rules, example packages that must install on the current build, path traversal in archives |
check_language_pack.py | language packs with typo keys, which would otherwise do nothing at all |
check_translation.py | built in translations with missing keys or broken placeholders |
| Koda Lab on a device | the whole bridge at runtime with raw answers, plus storage quota and rate limits |
| The packaging tools | unknown view ids, missing files and malformed colours before a package leaves your machine |
| The mirror publish script | instance data on its way into the public repository |
Making a release
A release is a version bump, a build, an entry in the releases table and a banner. The order matters, because the app reads the version from the database, not from the store.
- Raise
versionCodeandversionNamein the Gradle file and build a signed bundle for the store and an APK for direct installs. - Add a draft row to
releaseswith the changelog, the channel and the banner, and leave it unpublished while you check it. - Publish: the flag goes up, the app's update screen starts offering the new version, and the bot announces it in Discord.
Users on an older build see the update screen with the changelog text, which is why the changelog is written for people rather than for machines.
Licensing, in plain words
The project is free software under the GNU General Public License v3, and only that. Source files carry a short header that points at the license text. If you fork and publish, keep those headers and the license, and your users keep the same freedoms you got.
Third party components are listed twice on purpose: inside the app under Licenses in the settings, and on the website at host.kodanetwork.eu/licenses.html, so people can read them without installing anything. When you add a dependency, add its license text there as well.
Extensions are not covered by this license. They are separate works that run against a documented interface, and every published extension declares its own license, which the app shows to the user. That is why the license field is mandatory in the manifest and why the store recommends a source link.
When something goes wrong
Most problems have a short answer, and the app usually says it before you have to guess.
| Symptom | What to check |
|---|---|
| Server starts and stops again immediately | the console tab, the crash card names the cause and offers a fix |
| Players cannot join | the join address on the dashboard, and whether the server shows online |
| The server stops while the screen is off | background permissions, the app asks for battery exceptions during setup |
| Downloads fail | the network alarm, and the verbose log under Log in the console tab |
| An update does not appear | the app version in the settings, and whether the release is published |
| A plugin breaks the start | the plugins tab, disable the newest jar and read the console again |
When in doubt, the debug log screen copies everything into a support ticket in one tap, which is faster than describing symptoms.
Questions that come up often
Does a server keep running when the app is closed?
Yes. The server runs in a foreground service with a notification, and the app asks for battery exceptions during setup so Android does not stop it. The console and the dashboard keep working through KodaDash while the phone sleeps.
Do I need an account?
No. An account adds the website dashboard, recovery on a new device and the family features, but hosting works without one.
Is my world uploaded anywhere?
No. Worlds stay on the device unless you start a transfer to another device, and that transfer runs directly between the two phones.
Why is the app not on the store for every country?
Because hosting needs a device with enough memory and a modern WebView, and the store listing states the requirements honestly instead of collecting refunds.
Can I host more than one server?
Yes, as long as memory allows. The app tracks the budget, and hibernation exists so inactive worlds cost nothing.
How do I get help?
Through a ticket in the app or the Discord server. The personal chat is there for things that are not about servers at all.
About these docs. Large parts of this documentation were drafted with the assistance of an AI language model (deepseek-flash-v4.1) and reviewed by the developer. The AI was used for the wording, the structure and the examples only. The KodaHosting Android application itself, its server hosting logic, the networking infrastructure and all executable code are written entirely by the developer and are not AI-generated; where this documentation describes code, the code is the source of truth. Legal responsibility for all content, including the AI-assisted sections, rests with the developer and their legal guardians, as set out in the Impressum.
Koda