The last part of the walkthrough is about what the game stands on: the game's own data, held in memory; the database, which remembers players between sessions; the event system, which records what happens; and the website, where players manage their accounts and the admins look after the server.
The game data, in memory
WildStar's client ships its game data as tables: every creature, spell, item, zone, quest and much more, in
files ending in .tbl. Bezgelor reads its game data from the same tables, so a creature's name, level range
and abilities on our server are the client's own.
-
Extraction, once. Python tools in
tools/tbl_extractor/read the client's.tblfiles (tbl_extractor.py) and write JSON intoapps/bezgelor_data/priv/data/(extract_game_data.py, which knows how to turn about 180 client tables into the files the server uses). The JSON is committed, so nobody needs the client to run the server. -
Provenance. Not everything comes from the client: spawn positions come from NexusForever's world database,
and some values we had to design. Generated records carry a provenance label,
reference,derivedorguess, and the reason for it (docs/design/provenance.md). The Where we differ page is built from those labels. -
Loading, at every start.
BezgelorData.Store(apps/bezgelor_data/lib/bezgelor_data/store.ex) is a process that, when the server starts, reads the files into ETS tables: about two hundred tables, one per kind of data, plus about thirty index tables such as "spells by base spell" or "quests by zone". The biggest files are cached in a compact binary form (BezgelorData.Compiler) so start-up is quicker. -
Reading, all the time. Any process can look data up directly, without asking the Store:
BezgelorData.Store.get(:creatures, id)returns{:ok, creature}or:error. The tables are set up for many readers at once (read_concurrency), which suits data that is written once at start-up and read millions of times after. Modules such asBezgelorData.Housing,BezgelorData.PathsandBezgelorData.Lootwrap the Store with lookups for one area of the game.
The database
Everything that changes as people play (accounts, characters, inventories, quests, guilds, mail, the marketplace, housing) is stored in PostgreSQL, through Ecto, Elixir's database library.
-
BezgelorDb.Repo(apps/bezgelor_db/lib/bezgelor_db/repo.ex) is the connection to PostgreSQL (port 5433 by default). -
Contexts. The rest of the server never writes database queries itself. It calls a
context module, one per area of the game, with functions named for what they do:
BezgelorDb.Characters.update_position,BezgelorDb.Accounts.consume_game_token,BezgelorDb.Inventory,BezgelorDb.Quests,BezgelorDb.Guildsand about forty more, all inapps/bezgelor_db/lib/bezgelor_db/. -
Schemas (
apps/bezgelor_db/lib/bezgelor_db/schema/, about eighty) describe each table and check changes before they are written. -
Migrations (
apps/bezgelor_db/priv/repo/migrations/, about seventy) create and change the tables, in order, so any copy of the database can be brought up to date withmix ecto.migrate. -
Two-factor secrets are stored encrypted (
BezgelorDb.Vault).
When a world system saves depends on the system: quests, experience and currencies are written as they change, a player's position at most every five seconds and on logout.
Saving player progress
A disk can be slow. On a busy day a single write took up to two and a half seconds while reads took a few
milliseconds, and when the zone waited for each write, loot reached the player seven seconds after they picked it
up. So loot, experience, credits and quest progress are written behind: the zone tells the client at once, and
the database catches up a moment later (BezgelorDb.WriteBehind, apps/bezgelor_db/lib/bezgelor_db/write_behind.ex).
- A queue per character. The zone puts the write in a queue, an ETS table, and each character has a writer process that works through its queue in order. The queue doesn't belong to the writer, so a writer that crashes is restarted and picks up where it stopped.
- What the client was told. Until a write lands, the state the client was told is kept next to the queue (the overlay), so a zone that reloads the character's bags sees them as they will be, not as the database has them yet. Anything that reads a character from the database first waits for that character's queue.
- Batches. A writer takes what is queued, up to 50 writes or a quarter of a second of work, and saves it in one database transaction: one wait for the disk instead of fifty. If one write fails, the batch is undone and its writes are tried again one at a time, so one bad write never holds up the others. A write that still fails after five tries is dropped, logged and shown to the admins.
- Trades, mail and the rest wait. Anything that moves items or credits between characters or out of the game (trades, mail, the marketplace, vendors, salvage, crafting, the guild bank, housing plugs, quest turn-ins) first waits for the queues of the characters involved, then saves at once and for certain. That is why a crash can roll a player back a little but can never copy an item or credits.
What a crash costs:
| What happens | What is lost |
|---|---|
A clean stop (systemctl stop bezgelor) |
Nothing: players are logged out and every queued write is saved before the database connection closes |
The server program is killed (kill -9, out of memory) |
What was still queued, normally less than a second of each player's loot, experience and quest progress |
| The database or the machine crashes | Also up to about the last half second of saved progress (the database confirms these writes before they reach the disk, which is what makes them fast); never trades or mail, and nothing is corrupted |
The admin metrics page shows how much is queued, how old the oldest write is and how far behind the writers
are; docs/architecture.md has the details, and the self-hosting guide
has what a server operator needs to know.
The event system
bezgelor_events keeps one record of what happens in the game: logins, kills, quest completions, trades, admin
actions and about two hundred other kinds of event. There is one way in,
Bezgelor.Events.emit(name, attributes) (apps/bezgelor_events/lib/bezgelor/events.ex), and it never blocks
the caller and never fails:
-
The name must be in the list of known events (
Bezgelor.Events.Taxonomy). - Who and where (the character, the connection, the zone) are filled in from the calling process's log metadata, so a zone only passes what is specific to the event.
- The event also fires as a telemetry event, which feeds the server's metrics and alerts.
-
Ordinary events go to
Bezgelor.Events.Buffer, which writes them in batches (500 events or one second, whichever comes first) throughBezgelor.Events.Sink.Postgresto thegame_eventstable. If the database falls behind, the buffer drops and counts events past a limit rather than grow without end. High-volume events are summed per minute instead (Rollups), and a detailed debug record is kept only for characters an admin is investigating (Debug).
Chat text and secrets never go into the stream. Players can see and download their own events on the
website's Activity page (Bezgelor.Events.PlayerView), and the admins explore all of them on the event
stream pages. The design is in docs/plans/2026-10-11-event-stream.md.
Metrics and logs
BezgelorWorld.Telemetry keeps the server's metrics in memory (zone tick times, packets sent, players
online), samples them every ten seconds and raises alerts. They show on the admin pages and on the live
dashboard (/admin/dashboard, Phoenix LiveDashboard). Log lines start with a tag for the system they come
from, such as [Loot], and carry the account, character, connection and zone as metadata, so one player's
session can be followed through the logs. docs/observability.md lists the metrics.
The website and the admin pages
bezgelor_portal is the website, on port 4000, written with Phoenix and
LiveView. It has three parts (apps/bezgelor_portal/lib/bezgelor_portal_web/router.ex):
-
Public pages: the home page, features, news, download, help, and these Workshop pages, which are
Markdown files from
docs/community/turned into HTML when the site is built (BezgelorPortalWeb.CommunityDocs). - Account pages for players: their characters, their activity, their settings and two-factor login.
-
Admin pages (
live/admin/, about forty LiveViews): accounts, roles, characters, guilds, the economy, the marketplace, mail, chat, loot, instances, housing, zone scripts, metrics, logging, the event stream, bug reports and more. Each needs a permission (BezgelorDb.Authorization, roles made of permissions), every admin page needs two-factor login, and admin actions are written to an audit log, which also feeds the event stream.
Because the website runs in the same node as the game, it talks to the world server by calling functions in
BezgelorWorld.Portal (apps/bezgelor_world/lib/bezgelor_world/portal.ex): who is online, kick, broadcast a
message, grant an item, reload zone scripts. LiveView pages update in the browser as things change, without the
page being reloaded.
bezgelor_api is a much smaller web service on port 4002 (apps/bezgelor_api/lib/bezgelor_api/router.ex): the
server's status, its zones and the players online, and health checks (/health) that check the three game
listeners, the database, the zone tick, saving player progress and the active alerts.
Tests
Every app has its tests next to its code, in apps/<app>/test/. scripts/test_all.sh runs them all and prints a
table of results. Tests that need the database get a private, rolled-back transaction each
(BezgelorDb.DataCase), so they can run side by side. The slowest and most thorough are the end-to-end tests in
apps/bezgelor_world/test/e2e/, where headless players log in, fight, loot and quest through the real protocol
while the client oracle checks every
packet they receive. Interesting runs are published as test reports.
Where to look
-
apps/bezgelor_data/lib/bezgelor_data/store.exandtools/tbl_extractor/: the game data. -
apps/bezgelor_db/lib/bezgelor_db/: contexts;schema/: tables;priv/repo/migrations/: changes;write_behind.exandwrite_behind/: saving player progress. -
apps/bezgelor_events/lib/bezgelor/events.exandevents/: the event system. -
apps/bezgelor_world/lib/bezgelor_world/telemetry.ex: metrics and alerts. -
apps/bezgelor_portal/lib/bezgelor_portal_web/: the website (router.ex,live/admin/). -
apps/bezgelor_world/lib/bezgelor_world/portal.ex: what the website can ask of the world server.
That is the end of the walkthrough. Why Elixir & OTP explains why the server is built this way, and the glossary collects the words used along the way. Back to the overview.