A modern platform for persistent browser games, and the gangster game built
on it. One codebase, four faces: GL3_PROFILE=gl3 (the default) is the
flagship hybrid — everything below combined, curated; GL3_PROFILE=v2 is the
successor to Gangster Legends V2;
GL3_PROFILE=mccodes is the MCCodes-parity game; and GL3_PROFILE=framework
is the game-agnostic engine, the same module shape as
openPBBG (the GL2 framework
without the gangster game). Existing V2, openPBBG and MCCodes databases
migrate in one command, players and passwords included. Documentation lives at
docs.gl3.dev: tutorials, how-to guides, full API/DTO
reference, and the design-decision record. A live demo runs at
game.gl3.dev.
The whole GL2/MCCodes lineage is aging the same way, openPBBG included:
int(11) money columns overflowing past ~2.14B on long-running games, and
sha256 password hashes with no work factor. GL3 is the move-off path for
that installed base: bigint money, argon2id, real foreign keys
(see Migrating from V2 or openPBBG and the
V2 comparison).
Stack: Node.js 22 · TypeScript (strict) · Fastify 5 · PostgreSQL 16 · Redis 7 (BullMQ + pub/sub) · WebSockets · React 18 + Vite · a first-party plugin SDK
docker compose --profile app upThat command (after setting GL3_NPM_TOKEN in .env — the plugins
installer's registry credential) stands up the whole game from the published
images: Postgres and Redis (internal to the compose network), a one-shot
migrate container that asserts the schema, a one-shot plugins installer
(filling the plugins-data volume from PLUGIN_PACKAGES), the server
(starter content seeded at boot), the web bundle, and an nginx router keeping
the browser same-origin (deploy/nginx.conf). Open http://localhost:8080 and
register; the first player becomes Administrator. With EMAIL_DRIVER at its
default, the email verification link prints to
docker compose --profile app logs server.
Hosting elsewhere: set GL3_PUBLIC_ORIGIN (the URL players reach the game
at, which feeds the CORS allowlist the WebSocket gateway also checks) and
GL3_PORT. Want the engine without the gangster
game? GL3_PROFILE=framework boots the openPBBG-shaped platform instead
(see Game modes).
The same file without the profile is the development database pair:
docker compose up -d (what npm run db:up runs) starts only Postgres and
Redis. CI publishes the images to GHCR on every push to main
(ghcr.io/gl3pbbg/gl3-server, ghcr.io/gl3pbbg/gl3-web).
npm install
npm run db:up # Postgres 16 + Redis 7 via docker-compose.yml
npm --workspace @gl3/server run db:migrate
npm --workspace @gl3/server run dev # http://localhost:3000
npm --workspace @gl3/web run dev # http://localhost:5173The server reads plain environment variables (nothing in the repo auto-loads
a .env file), so export them first. .env.example documents every knob the
server understands:
export DATABASE_URL=postgres://gl3:gl3@localhost:5432/gl3
export REDIS_URL=redis://localhost:6379npm run db:up isn't required; the project is developed against natively
installed Postgres and Redis too. All that matters is that DATABASE_URL and
REDIS_URL point at a Postgres 16 and Redis 7 instance.
Run npm run verify (typecheck + tests) before every commit; integration tests
need Postgres and Redis up, and apps/migrate's tests additionally need
MYSQL_ADMIN_URL pointing at a MariaDB/MySQL instance (npm run test:nodb
is the subset that needs none of them).
Run migrations against your dev database, not just in tests. npm --workspace @gl3/server run db:migrate must be run manually against whatever
DATABASE_URL your running server uses. Test databases are created fresh per
test file and migrate automatically, so it's easy to have a fully green test
suite while your dev database is stale: the server boots fine but every
background job (crime resolution, cooldowns, anything going through BullMQ)
silently fails because the tables it needs don't exist yet.
apps/server: Fastify API, WS gateway, BullMQ workers, plugin loaderapps/web: React client (categorized nav, plugin page renderer, live event feed)apps/migrate:gl3-migrate, the V2 → GL3 migration CLIpackages/shared: zod event contracts and DTOs, shared by both appspackages/plugin-sdk: the SDK plugins are written againstpackages/plugins/*: twenty-seven first-party plugins, split framework/gameplay/MCCodes byGL3_PROFILE(see below)examples/hello-plugin: a minimal, annotated example plugindeploy/: the nginx router config the composeappprofile mountsmanual/: the VitePress source of docs.gl3.dev
Every bundled module is a plugin in packages/plugins/, split into three
sets that GL3_PROFILE (gl3 | v2 | mccodes | framework, default
gl3) combines:
- framework (every profile): the game-agnostic engine, openPBBG's
module list:
ranks,notifications,news,bank,mail,forum,inventory,membership. - gameplay (
v2andgl3): the gangster game on top:crimes,bullets,travel,gangs,combat,bounties,detectives,oc,theft,properties,casino,blackjack. - MCCodes family (
mccodesandgl3):mccodes-attributes(pools and gym-trained stats) plusgym,houses,education,jobs,temple,progression. Themccodesprofile also pulls incrimes,combat,travelanddetectives, where its mechanics live.
A framework boot skips jail/hospital, the sentence sweeper, the wealth tax
and the gameplay seeds; gameplay and family plugins can be added back
individually with PLUGIN_IDS (GL3_PROFILE=framework PLUGIN_IDS=crimes).
Cross-plugin requirements (combat needs inventory and detectives, ...)
are declared on the manifests and enforced at boot.
Third-party plugins load through PLUGIN_IDS (compiled into the server) and
PLUGIN_PACKAGES (imported at boot from outside the build); .env.example
and the plugin guide cover
both. Writing one starts with examples/hello-plugin.
The session token never appears in a URL. A client that wants to open the
WebSocket first calls the authenticated POST /api/ws/ticket, which mints a
short-lived (~30s), single-use handshake ticket in Redis. The client then
connects to /ws?ticket=<ticket>; the gateway consumes the ticket atomically
and invalidates it on first use, so it can't be replayed even if it leaks into
a proxy or access log. The gateway also validates the Origin header against
the CORS allowlist to prevent cross-site WebSocket hijacking.
gl3-migrate converts a live Gangster Legends V2 MySQL database into a GL3
PostgreSQL one (players, passwords, cash, gangs, inventories) in one command:
gl3-migrate --mysql mysql://user:pass@host/v2db --pg postgres://user:pass@host/gl3db--dry-run runs every migrator inside a transaction that always rolls back
(use this first against a production V2 database); --report report.json
writes the full per-table machine-readable report; --town-combat-mode open|underground picks V2's everybody-hidden combat rules up front; and
--mccodes switches the source dialect to MCCodes v2. The
target needs a booted GL3 schema first (core migrations + one server boot for
plugin tables); see apps/migrate/README.md and the
operator guide for the walkthrough.
openPBBG databases are the same story with less data: the account tables are
byte-identical to V2's, and the game tables (crimes, gangs, cars, ...)
simply do not exist. The same CLI handles them, skipping every games phase
with a report entry, and pairs naturally with a GL3_PROFILE=framework
target, whose absent p_* plugin tables are skipped the same way. See
The framework profile
for the walkthrough.
GL3 is not a schema copy. These changes are intentional, and the migration CLI maps across them:
| V2 | GL3 | Why |
|---|---|---|
users + userStats |
players + player_stats |
Same 1:1 split, kept for hot-row separation |
int(11) money (signed 32-bit) |
bigint everywhere |
Long-running V2 games hit the ~2.14B ceiling |
unix-epoch int(11) timestamps |
timestamptz |
Real dates, real time zones |
AUTO_INCREMENT integer PKs |
UUIDv7 PKs | The migrator keeps an id_map table for cross-references and idempotent re-runs |
| no foreign keys anywhere | real FKs with ON DELETE rules |
Orphans become impossible rather than merely common |
userStats.US_gang integer |
gang_members join table |
A player's gang is a relationship, not a column |
userStats.US_crimes dash-delimited string |
player_crime_skill rows |
V2 indexed the string by C_id - 1, not row order; GL3 makes that explicit as (player, crime, chance) rows |
items + itemEffects + itemMeta EAV |
items.effects / items.meta JSONB |
One row per item instead of three tables |
gangPermissions with no gang column |
gang_permissions with a real gang_id |
V2 implied the gang from the member's US_gang; GL3 stores it directly |
properties.PR_module varchar |
properties.plugin_id varchar |
Same string coupling, renamed to GL3's vocabulary |
all timers in userTimers |
typed jailed_until / hospital_until columns + generic player_timers |
Action-gating timers are promoted to real columns; everything else stays open-ended, since custom V2 modules add arbitrary timer keys |
sha256(U_id . password) passwords |
argon2id, lazily upgraded | No forced resets (see below) |
V2 stored sha256(U_id . plaintext), unsalted beyond the user id, with no work factor. Migration
copies that hash verbatim into players.legacy_password_sha256 and the V2 integer id into
players.legacy_v2_id, leaving players.password_hash null. On the first successful login GL3
verifies the password against the legacy formula, rehashes it with argon2id into password_hash,
and nulls legacy_password_sha256. Players never notice, and nobody is forced to reset.
- M0 Scaffold, M1 Auth + vertical slice, M2 Core loop parity, M3 Social:
complete. The economy invariant (
sum(ledger) == balance) is enforced byapps/server/test/economy-invariant.test.ts. - M4 Migration CLI: complete.
apps/migrateruns 20 migrators over an 8-phase pipeline for a V2 source, idempotent viaid_map, plus an MCCodes dialect (--mccodes) with its own migrator set. - M5 Plugin SDK: foundation, web page renderer and all nine V2 module ports complete; the gameplay clusters listed under Plugins above have shipped on top of it, along with admin (role → module grants) and a themed, categorized game shell.
- Framework profile:
GL3_PROFILE=frameworkruns the engine without the gangster game (openPBBG parity, plugin-shaped gameplay, boot-enforced cross-pluginrequires), andgl3-migrateaccepts openPBBG sources and framework targets.
This list is a summary, not the whole story — the gameplay clusters under Plugins above each shipped with their own tests as the behaviour record.
- docs.gl3.dev: tutorials, guides (including the working conventions and lock-order rules in Create a plugin), reference, and design decisions.
SPEC.md: what to build.manual/: the docs site source; documentation changes ship in the same PR as the code they describe.
MIT. See LICENSE.