Run Multiple Universes on One Server
A single django-moo deployment can host independent worlds — separate object hierarchies, separate player accounts, separate game state — by serving each one from a different hostname. This is the “multi-universe” or “multi-site” mode.
Each universe is a Django Site row. Every database query that goes
through moo.core.models.object.Object.objects is scoped to the
active Site by the moo.core.managers.SiteManager. Wizards are
scoped per Site by default; opt-in cross-universe rights go through the
moo.core.models.auth.UniversalWizard table.
The SSH protocol has no equivalent of TLS SNI or HTTP Host (see
RFCs 4253/4254/8308), so the dialed hostname cannot reach the server
on its own. DjangoMOO resolves the active Site by either (a) a suffix
encoded in the SSH username — ssh user+sitedomain@host — or (b) an
interactive picker shown after authentication.
Bootstrap a second universe
Each Site has its own copy of the System Object, Wizard, root classes,
and game state. To add a second universe pointing at hostname
test.example.com:
docker compose run webapp manage.py moo_init \
--bootstrap default --hostname test.example.com
--hostname looks up (or creates) a Site row with that domain and
makes it the active site for the bootstrap run. Run moo_init again
with a different hostname (and a different --bootstrap if desired)
to add a third, fourth, etc.
The default site is localhost unless SITE_ID is set in Django
settings. Connections without an explicit hostname (or to a hostname
not registered in Site.objects) fall back to that default.
Connect to a specific universe
Direct SSH
Encode the Site domain in the SSH username with a + delimiter:
ssh -p 8022 alice+zork.example.com@your-host
The server splits alice+zork.example.com into the Django user
alice and the Site zork.example.com. Without a suffix, the server
prints the universes available to your account and prompts you to
pick one — useful the first time, but routine connections should use
the suffix to skip the prompt.
If the suffix names a Site that doesn’t exist, the picker runs anyway so you can fall back to a real universe.
Webssh / browser
When you visit https://zork.example.com/ (with a Site row that
matches that domain), the Django POST proxy reads the browser’s
Host header and rewrites the SSH username to include the matching
suffix before handing the connection to webssh. The terminal prints
Connected to universe: zork.example.com so you can confirm the
routing succeeded.
If the browser hostname doesn’t match a Site row, no suffix is
injected and you’ll see the picker (or the default site for
single-universe accounts).
Per-site wizard accounts
By default, a wizard avatar in one universe is just a player in another.
Each Player row carries a site foreign key, and the unique
constraint on (user, site) allows the same Django User to have
distinct avatars in different universes.
Create a per-site wizard by bootstrapping that universe (moo_init
creates a Wizard Object on each fresh site) and then logging in via
the matching hostname.
Cross-universe wizard rights
For a single Django user to act as wizard on every universe — useful for operators — mark the user as a UniversalWizard:
docker compose run webapp manage.py moo_make_universal alice
On the next SSH connection to any site, if Alice doesn’t have a Player row for that site yet, the server auto-provisions a wizard avatar + Player for her on that site. Existing avatars are not modified.
To revoke:
docker compose run webapp manage.py moo_make_universal alice --remove
User.is_superuser alone does not grant cross-universe rights —
universal wizard status is opt-in via this command.
Site-scoped queries
Object.objects, Property.objects, and Verb.objects all go through
SiteManager and silently filter to the active site. To query across
sites — for diagnostics, migrations, or admin tooling — use
Object.global_objects (and the analogous globals on other models).
Inside a verb, the active site is whatever the calling player connected
to; you don’t need to pass it explicitly. Inside management commands and
Celery tasks, use code.ContextManager(player_or_wizard, ..., site=...)
to set the active site for the duration of the block.
When not to use multi-universe
Multi-universe is overkill for:
A single game world with a development hostname and a production one; use Django settings (
ALLOWED_HOSTS, etc.) instead, since both hostnames point at the same Site.“Areas” or “themed zones” within one game; those are just rooms parented appropriately.
It earns its complexity when you genuinely need separate persistent
worlds — one production game, one external dataset (the zork1
bootstrap shipped with moo-agent is one example), one private
playtest — each with independent objects and players.
Diagnostic commands
Inspect the current site set:
docker compose run webapp manage.py shell
>>> from django.contrib.sites.models import Site
>>> for s in Site.objects.all():
... print(s.pk, s.domain, s.name)
Find every Player on a given site:
>>> from moo.core.models.auth import Player
>>> Player.objects.filter(site__domain="zork.example.com")
List all UniversalWizard accounts:
>>> from moo.core.models.auth import UniversalWizard
>>> UniversalWizard.objects.values_list("user__username", flat=True)