Architecture and integration reference. For what Lectern does and why, see
functional.md.
| Layer | Choice | Notes |
|---|---|---|
| Backend | Python 3.11+ / FastAPI | Async, native WebSocket support |
| ASGI server | uvicorn | |
| HTTP client | httpx (async) | All external service calls |
| ORM / DB | SQLModel on SQLite | Pydantic + SQLAlchemy; single-file DB |
| Scheduling | APScheduler (AsyncIOScheduler) |
Cron actions |
| Process mgmt | asyncio subprocess + psutil | One async task per server, no thread-per-server |
| Frontend | React + TypeScript (Vite) | SPA, JSON API + WebSocket |
| Styling | Tailwind (minimal) | Simplicity over polish |
| Packaging | Docker / docker-compose | One-command LAN deploy |
Design principle — async-first. Unlike Crafty's thread-per-server model, each Minecraft
process is driven by an asyncio subprocess with a background task pumping stdout directly to
WebSocket subscribers. This is the natural fit for FastAPI and keeps concurrency simple.
┌────────────────────┐ HTTP JSON ┌─────────────────────────┐
│ React SPA (Vite) │ ─────────────► │ FastAPI routers │
│ - dashboard │ WebSocket │ (api/*) │
│ - create wizard │ ◄────────────► │ │
│ - server detail │ console └───────────┬─────────────┘
└────────────────────┘ │
▼
┌───────────────────────────── Services ──────────────────────────────┐
│ ServerManager ContentManager Installers BackupManager │
│ (asyncio procs) (mods/packs/ (vanilla/ (zip/restore) │
│ + ConsoleHub) updates) fabric/quilt/paper + Java) │
│ Scheduler (APScheduler) Providers (httpx clients, cached) │
└───────────────┬───────────────────────────────┬──────────────────────┘
▼ ▼
SQLite (SQLModel) External services
(Modrinth, Vanilla Tweaks,
Mojang, Fabric, Quilt,
Paper, Adoptium)
main.py App factory; lifespan: init DB, start scheduler,
reconcile statuses, auto-start flagged servers.
config.py Settings: LECTERN_DATA dir, host/port, defaults.
db.py SQLModel engine + session dependency.
models.py ORM tables (see §4).
ws.py ConsoleHub: per-server subscriber sets + broadcast.
servers/
process.py ServerProcess: asyncio subprocess, stdout pump, stdin.
manager.py Registry {server_id: ServerProcess}; lifecycle ops.
install.py Create-server pipeline (jar + Java + files + command).
content/
manager.py Install/remove/toggle/update; manifest; .mrpack import.
resourcepacks.py Vanilla Tweaks generation + pack.mcmeta parsing.
backups.py BackupManager: zip/list/restore/prune.
scheduler.py APScheduler wiring from Schedule rows.
providers/ httpx clients (see §6): mojang, fabric, quilt, paper,
modrinth, vanillatweaks, adoptium.
api/ Routers: servers, content, catalog, backups, schedules.
- Typed API client (
fetchwrapper) + shared model types. useConsoleSocket(serverId)hook for the live console WebSocket.- Pages: Server list, Create wizard, Server detail with tabs — Console, Properties, Mods/Plugins, Resource Packs, Backups, Schedule.
The first version ships Fabric + Modrinth + Vanilla Tweaks only, but the deferred items
(CurseForge, Quilt/Paper/Forge, Bedrock — see functional.md §3) are added by
implementing an interface, not by editing core logic. Two seams matter:
ServerTypeprovider (servers/install.py→ a registry keyed bytype). Each provider knows how to: resolve available versions, download/prepare the server jar, and build the launch command.vanillaandfabricare the first implementations;quilt,paper,forge,neoforgeslot in later. AServer.editionaxis (java|bedrock) is reserved for Bedrock, which swaps the runtime/launch strategy but reuses the lifecycle/backup/schedule machinery.ContentSourceprovider (providers/→ a registry keyed bysource). Interface:search,list_versions,resolve_download(+ hashes),check_update.modrinthis the first implementation;curseforgeis added later behind the same interface (it needs an API key and uses SHA1 fingerprints instead of SHA512 — differences stay inside that provider).
The DB already carries these axes (Server.type, ContentItem.source in §4), so no schema change
is needed to introduce new providers — only new provider classes and UI options.
A single configurable data directory, LECTERN_DATA:
LECTERN_DATA/
├── lectern.sqlite # application database
├── java/
│ └── temurin-{8,16,17,21}/ # shared JREs, keyed by Java major version
├── cache/ # provider catalogs, downloaded jars/mod files
├── backups/
│ └── {server_id}/{timestamp}.zip # OUTSIDE server dirs (see backup guard, §7)
└── servers/
└── {server_id}/
├── server.jar | paper-*.jar | fabric-server-launch.jar
├── eula.txt
├── server.properties
├── mods/ # Fabric/Quilt (disabled files suffixed .disabled)
├── plugins/ # Paper
├── resourcepacks/
├── world/ ...
└── .lectern/
└── manifest.json # per-server content manifest (see below)
Source of truth for installed content and the basis for update checks. It mirrors the intent of
Prism's packwiz .pw.toml files (which is why content stays updatable rather than being opaque
jars). Per item:
{
"items": [
{
"kind": "mod", // mod | plugin | resourcepack | modpack
"source": "modrinth", // modrinth | vanillatweaks
"project_id": "P7dR8mSH",
"version_id": "abc123",
"slug": "fabric-api",
"name": "Fabric API",
"filename": "fabric-api-0.92.0.jar",
"side": "both", // client | server | both
"sha512": "…",
"game_version": "1.20.1",
"loader": "fabric",
"enabled": true
}
]
}Key rows are mirrored into SQLite (ContentItem) for fast querying/UI, but the manifest lets a
server be re-hydrated or inspected independently.
The manifest is a reconciliation input, not a log (pattern borrowed from mc-image-helper —
see references/mc-image-helper.md). Content operations
compute the desired file set, diff it against the manifest, delete files present in
old-but-not-new, then write the new manifest. Install/remove/update and .mrpack (re-)import are
therefore convergent: re-running an import upgrades in place and removes files the new pack
version no longer ships.
| Table | Fields (summary) |
|---|---|
| Server | id (uuid), name, path, type (vanilla|fabric|quilt|paper), mc_version, loader_version, java_major, java_path, port, memory_mb, jvm_args, auto_start, crash_restart, stop_command (default stop), shutdown_timeout, status, created_at |
| ContentItem | id, server_id (fk), kind, source, project_id, version_id, slug, name, filename, sha512, channel (release|beta|alpha — allowed release channel for updates), enabled, installed_at |
| Backup | id, server_id (fk), filename, size_bytes, created_at, trigger (manual|scheduled) |
| Schedule | id, server_id (fk), action (start|stop|restart|backup), cron, enabled |
| Setting | key, value (data dir, default memory, optional password hash later) |
status ∈ stopped | starting | running | stopping | crashed and is reconciled at startup.
ServerProcesslaunches the server viaasyncio.create_subprocess_exec(java, *jvm_args, "-jar", jar, "nogui", stdin=PIPE, stdout=PIPE, stderr=STDOUT).- A background task reads stdout line by line, appends to a bounded ring buffer (recent
history), and pushes each line to the
ConsoleHub, which fans it out to all WebSocket subscribers for that server. - Commands from the UI are written to the process stdin (
proc.stdin.write(cmd + "\n")). - Graceful stop sends the configured
stop_command(stop), waits up toshutdown_timeout, then falls back toterminate(), and finally kills the process tree viapsutilif needed. - Crash detection: an unexpected process exit sets status
crashed; ifcrash_restartis on, the server is restarted — capped (~3 consecutive attempts, counter reset once the server reachesrunning) so a boot-looping server doesn't restart forever (Crafty's lesson). - WebSocket endpoint
GET /ws/servers/{id}/console: on connect, replay ring-buffer history, then stream live lines; inbound text messages are treated as console commands. - Stats:
psutilon the process for CPU/memory; a Minecraft Server List Ping to127.0.0.1:{port}for online/max players and MOTD. Pull-based only: computed onGET /servers/{id}/stats, driven by frontend polling — no background stats loop in v1. The ping uses the port read fromserver.properties(the DB column can drift), and the parser must handle all MOTDdescriptionvariants (plain string,translate,extra[]) with the favicon optional (seereferences/crafty-4.md).
All calls go through thin async httpx clients under providers/, with catalog responses
cached in cache/. Downloads are checksum-verified where a hash is available.
First-version services: Mojang (§6.1), Fabric (§6.2), Modrinth (§6.5), Vanilla Tweaks (§6.6), Adoptium (§6.7). Roadmap services (documented now, implemented later behind the same provider interfaces): Quilt (§6.3), Paper (§6.4), and CurseForge (noted in §6.5).
- Version manifest:
GET https://launchermeta.mojang.com/mc/game/version_manifest_v2.json - Per-version JSON (URL from manifest) →
downloads.server.url= the vanilla server jar. - Used to populate the MC-version picker and download vanilla jars.
Base: https://meta.fabricmc.net/v2
- Game versions:
GET /versions/game - Loader versions for a MC version:
GET /versions/loader/{mc} - Ready-to-run server jar:
GET /versions/loader/{mc}/{loader}/{installer}/server/jar→ downloadable Fabric server launcher.
Base: https://meta.quiltmc.org/v3 (same shape as Fabric)
GET /versions/loader/{mc}for loader versions.- Server install may require running the Quilt installer jar rather than a direct server-jar
endpoint — this variance is isolated in
providers/quilt.py. (Flagged risk, see §9.)
Base: https://api.papermc.io/v2/projects/paper
- Versions:
GET /→versions[] - Builds for a version:
GET /versions/{version} - Latest build detail:
GET /versions/{version}/builds/{build} - Jar download:
GET /versions/{version}/builds/{build}/downloads/{jar}
Base: https://api.modrinth.com/v2 (send a descriptive User-Agent)
- Search:
GET /search?query={q}&facets={facets}&limit=…&offset=…facetsis a JSON array of AND-ed OR-groups, e.g.[["project_type:mod"],["categories:fabric"],["versions:1.20.1"]]project_type=mod|resourcepack|modpack; Paper plugins areproject_type:modwith loader categoriespaper/bukkit/spigot. Fabric/Quilt usecategories:fabric/categories:quilt.
- Project versions (for install/update):
GET /project/{id|slug}/version?loaders=["fabric"]&game_versions=["1.20.1"]→ each version hasid,version_number,files[](url,filename,hashes.sha512),dependencies[](project_id,dependency_type). - Update lookup by hash:
GET /version_file/{sha512}?algorithm=sha512. - Verification: SHA512 from
files[].hashes.sha512. .mrpackimport: the archive containsmodrinth.index.json(dependencies→ loader + MC versions;files[]→path,downloads[],hashes.sha512,env) plusoverrides/(config files copied verbatim).- CurseForge (roadmap): a future second
ContentSourcebehind the same interface. Basehttps://api.curseforge.com/v1(requires an API key); searchGET /mods/search?gameId=432&…, filesGET /mods/{id}/files, and SHA1-based verification/fingerprints. Its differences (key, hash algorithm, redistribution flags) stay contained in the provider.
Base: https://vanillatweaks.net (no official/stable API — isolate + fail gracefully)
- Catalog:
GET /assets/resources/json/{mc}/rpcategories.json— categories and pack ids. - Generate:
POST /assets/server/zipresourcepacks.phpwith form fieldpacks={"category":["packId", …]}andversion={mc}→ JSON{ "status": "success", "link": "/download/…zip" }. - Download:
GET https://vanillatweaks.net{link}. - (Datapacks / crafting tweaks have parallel
zipdatapacks.php/zipcraftingtweaks.phpendpoints — resource packs first.)
- Download JRE:
GET https://api.adoptium.net/v3/binary/latest/{java_major}/ga/{os}/{arch}/jre/hotspot/normal/eclipse(redirects to the archive). - MC → Java major mapping:
≤1.16 → 8,1.17 → 16,1.18–1.20.4 → 17,1.20.5+ → 21. - Extract into
LECTERN_DATA/java/temurin-{major}/, shared across servers.
GET /catalog/server-types— selectable types/flavours (initiallyvanilla,fabric).GET /catalog/minecraft-versions?type={type}— the Minecraft versions available for that type (Vanilla → Mojang release list; Fabric → Fabric/versions/game), newest first. EachServerTypeprovider supplies its own version list, so the wizard's version choice is always scoped to the chosen type.GET /catalog/loaders/{fabric|quilt}/{mc}— loader builds for a given Minecraft version.GET /catalog/paper/{mc}— Paper builds (roadmap).
GET /servers·POST /servers(create) ·GET /servers/{id}·DELETE /servers/{id}POST /servers/{id}/action/{start|stop|restart|kill}GET /servers/{id}/statsGET/PATCH /servers/{id}/properties(server.properties)POST /servers/{id}/eula(accept)WS /ws/servers/{id}/console
GET /content/search?server_id=…&query=…— Modrinth search scoped to the serverGET /content/projects/{id}/versions?server_id=…GET /servers/{id}/content— installed list (+ update flags)POST /servers/{id}/content— install{project_id, version_id}(+ deps)PATCH /servers/{id}/content/{item_id}— enable/disable/updateDELETE /servers/{id}/content/{item_id}GET /servers/{id}/content/updatesPOST /servers/{id}/modpack— import.mrpack
GET /servers/{id}/vanillatweaks/categoriesPOST /servers/{id}/vanillatweaks—{ packs: {category: [ids]} }→ generate + install
GET /servers/{id}/backups·POST /servers/{id}/backups(create)POST /servers/{id}/backups/{backup_id}/restoreDELETE /servers/{id}/backups/{backup_id}
GET /servers/{id}/schedules·POST …·PATCH …/{sid}·DELETE …/{sid}
- Create: zip the server directory, excluding a configurable set of paths, writing to
LECTERN_DATA/backups/{server_id}/{timestamp}.zip. - Path-traversal guard: the backup destination is validated to be outside the server directory before writing/restoring (borrowed from Crafty's approach) to avoid recursive or destructive archives.
- Restore: stop the server → validate the archive (name/format) → replace server data → report success/failure.
- Retention: optional prune to keep the last N backups.
- Target: a single Linux host on a trusted LAN.
- Docker-compose bundles the FastAPI backend (serving the built React SPA as static files)
and mounts
LECTERN_DATAas a volume. One command:docker compose up. - Config via environment variables (
LECTERN_DATA, bind host/port). - No TLS/auth assumed at launch (LAN trust); a single optional password may be added later.
- Quilt server install is the least standardized of the four types; may need the Quilt
installer jar rather than a direct server-jar endpoint. Isolated in
providers/quilt.py. - Vanilla Tweaks has no official API; the
zipresourcepacks.phpcontract can change without notice. Keep isolated and degrade gracefully. - Long downloads (server jars, JREs) must run as tracked background tasks with progress surfaced over WebSocket, so create-server isn't a blocking spinner.
- Modrinth rate limits / User-Agent policy — respect their requirements; cache catalogs.
- Java major mapping must be revisited as new Minecraft versions raise Java requirements.
- Unit (pytest): provider clients against recorded fixtures; MC→Java mapping; manifest and
update-diff logic; backup path-traversal guard;
.mrpackparsing. - Integration (marked): real process start/stop, console streaming, end-to-end create → run.
- Manual per-phase verification: create a real server of each type, install a known mod (e.g. Fabric API), generate a Vanilla Tweaks pack, back up and restore — observing live behaviour in the browser (this is a server manager; behaviour must be seen, not just asserted).