Observed since 20 Oct, 17:07 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Wormhole Mapping and Navigation Plugin for Alliance Auth
Yet Another Wormhole Navigator - a live, collaborative wormhole-chain mapping plugin for Alliance Auth (AA) - a Pathfinder/Tripwire-style shared map that a corp or alliance builds up together in real time, with optional live character-location tracking.

As a grumpy null sec player first, wormhole player rarely, i have never found a tool that did what we wanted without having to hack it/modify it to expose the settings to share stuff how we wanted or have a permission system that worked for us. The goal here is not to recreate Wanderer, Pathfinder or any other of your favorite tools, they are all very good tools if they work for you please go use them and support the amazing developers that created them!
This is unapologetically written from the POV of a WH illiterate Null sec F1/2 Monkey trying to help his group do stuff. Having said that if there is something that you would like to see in the tool and it doesn't degrade our use case i am very happy to work on it.
| Sharing a map | Global character tracking |
|---|---|
![]() |
Bulk region import
This plugin is quite different to other Alliance auth plugins as it uses websockets! Please be sure to read the instructions properly otherwise YMMV!
eve_sde) for solar system/region/wormhole-type reference data - will be installed automatically as a dependency however it requires it's own setup consult the link for more details.pip install aa-wh-mapper
Add "channels", and "wh_mapper" to INSTALLED_APPS in your project's settings/local.py
INSTALLED_APPS += [
"channels", # Only if not already added
"eve_sde", # Only if not already added
"wh_mapper",
]
Wire up the channel layer used for live map updates:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [
{
"address": f"redis://localhost:6379/5",
"socket_timeout": None,
}
]
}, # a DB index separate from Celery's broker
},
}
Merge YAWN's websocket routes into your project's ASGI application (asgi.py):
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
django_asgi_app = get_asgi_application()
from wh_mapper.routing import websocket_urlpatterns
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
}
)
Serve that ASGI application with an ASGI server (e.g. daphne) instead of (or in front of, via your reverse proxy) your usual WSGI server - Alliance Auth's HTTP routes keep working unchanged through django_asgi_app above.
Recommended: keep gunicorn serving everything else, and run daphne as a second process just for /ws/ - it's a much smaller surface than your full HTTP load (one connection per open map, not per request), and this keeps gunicorn's mature sync worker model for the rest of Alliance Auth's ordinary request/response traffic:
daphne -b 127.0.0.1 -p 8001 your_project.asgi:application
Then route only /ws/ to it in nginx, leaving everything else on your existing gunicorn upstream:
upstream wh_mapper_daphne {
server 127.0.0.1:8001;
}
server {
# ... your existing server block (listen, server_name, ssl, etc.) ...
location /ws/ {
proxy_pass http://wh_mapper_daphne;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Websocket connections are long-lived - nginx's default proxy
# timeout would otherwise silently drop an idle one and force a
# reconnect.
proxy_read_timeout 86400;
}
location / {
proxy_pass http://your_gunicorn_upstream;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
/ws/ is matched as a prefix (not just wh_mapper's own /ws/wh-mapper/maps/<id>/) so any other Channels-based plugin's websocket routes are covered by the same rule without further nginx changes.
If daphne isn't (yet) proxied on the same origin as the rest of your site (e.g. you're pointing straight at port 8001 during setup, before nginx is wired up), set WH_MAPPER_WS_ORIGIN in settings/local.py to where it's actually reachable, so the frontend knows where to open its websocket connection:
WH_MAPPER_WS_ORIGIN = "ws://localhost:8001"
Leave it unset for the normal setup, where a reverse proxy routes /ws/ to daphne on the same origin as everything else.
The menu entry and URLs (/wh-mapper/...) register themselves automatically once the app is installed - no further urls.py changes needed.
python manage.py migrate
python manage.py collectstatic
Add these to CELERYBEAT_SCHEDULE in settings/local.py:
CELERYBEAT_SCHEDULE["wh_mapper_poll_tracked_character_locations"] = {
"task": "wh_mapper.tasks.poll_tracked_character_locations",
"schedule": crontab(
minute="*"
), # a fallback kick - it self-reschedules every ~10s while anyone's online
}
CELERYBEAT_SCHEDULE["wh_mapper_age_wormhole_connections"] = {
"task": "wh_mapper.tasks.age_wormhole_connections",
"schedule": crontab(minute="*/5"),
}
CELERYBEAT_SCHEDULE["wh_mapper_refresh_system_sovereignty"] = {
"task": "wh_mapper.tasks.refresh_system_sovereignty",
"schedule": crontab(minute="0"),
}
CELERYBEAT_SCHEDULE["wh_mapper_prune_stale_map_presence"] = {
"task": "wh_mapper.tasks.prune_stale_map_presence",
"schedule": crontab(
minute="*/2"
), # catches a connection whose disconnect() never fired (e.g. a worker crash)
}
CELERYBEAT_SCHEDULE["wh_mapper_prune_stale_routes"] = {
"task": "wh_mapper.tasks.prune_stale_routes",
"schedule": crontab(minute="30"), # deletes any shared Route unviewed for 48h
}
CELERYBEAT_SCHEDULE["wh_mapper_sync_eve_scout_thera_turnur"] = {
"task": "wh_mapper.tasks.sync_eve_scout_thera_turnur",
"schedule": crontab(
minute="*/5"
), # matches eve-scout.com's own 5-minute Cache-Control on its public signatures feed
}
YAWN defines two permissions:
wh_mapper.basic_access - can access the app and create/use mapswh_mapper.admin_access - can manage or delete any map, not just their ownGrant basic_access to whichever states/groups should be able to use it.
Character tracking needs your EVE SSO application (in the developer portal) to include these scopes:
esi-location.read_location.v1esi-location.read_online.v1Once eve_sde's SDE data is imported, populate the wormhole-type catalog (mass/jump-mass/lifetime, derived from dogma data) with:
python manage.py wh_mapper_derive_wormhole_types
Re-run this after every SDE re-import to pick up any new wormhole types.
Populate each J-space system's fixed static wormhole connection(s) - shown as badges above a system node on the map - with:
python manage.py wh_mapper_import_system_statics
This data is bundled with the app (it isn't part of CCP's SDE), so it only needs running once; no SDE import required first.
If you're running Alliance Auth via its official docker-compose setup, the steps above map onto that repo's docker/ directory as follows - see its own docs/installation-containerized/docker.md for the general custom-package workflow this follows.
Add aa-wh-mapper and daphne to aa-docker/conf/requirements.txt (one package per line, a version pin recommended)
Add the same INSTALLED_APPS entries from step 2 above to aa-docker/conf/local.py. Point CHANNEL_LAYERS at the compose redis service, using a database index separate from the broker (0) and cache (1) it already uses:
INSTALLED_APPS += [
"channels",
"eve_sde", ## Setup this as per that applications instal details FIRST!
"wh_mapper",
]
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [
{
"address": f"redis://{os.environ.get('AA_REDIS', 'redis:6379')}/5",
"socket_timeout": None,
}
],
},
}
}
ASGI_APPLICATION = "myauth.asgi.application"
The base image only ships a wsgi.py, not an asgi.py Create aa-docker/conf/asgi.py:
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myauth.settings.local")
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
django_asgi_app = get_asgi_application()
from wh_mapper.routing import websocket_urlpatterns
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
}
)
then add it to the allianceauth-base anchor's volumes: in docker-compose.yml, alongside the existing celery.py/urls.py lines:
volumes:
- ./conf/local.py:/home/allianceauth/myauth/myauth/settings/local.py
- ./conf/celery.py:/home/allianceauth/myauth/myauth/celery.py
- ./conf/urls.py:/home/allianceauth/myauth/myauth/urls.py
- ./conf/asgi.py:/home/allianceauth/myauth/myauth/asgi.py ## this one is new
# ...rest of the volumes unchanged
Add a daphne service to serve it, mirroring the existing allianceauth_gunicorn service:
allianceauth_daphne:
container_name: allianceauth_daphne
<<: [*allianceauth-base]
entrypoint: ["daphne", "-b", "0.0.0.0", "-p", "8001", "myauth.asgi:application"]
expose:
- 8001
Then route /ws/ to it in docker/conf/nginx.conf, above the existing catch-all location /:
location /ws/ {
proxy_pass http://allianceauth_daphne:8001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Websocket connections are long-lived - nginx's default proxy timeout
# would otherwise silently drop an idle one and force a reconnect.
proxy_read_timeout 86400;
}
docker compose --env-file=.env up -d
docker compose exec allianceauth_gunicorn bash
allianceauth update myauth
auth migrate
auth collectstatic
Add the same CELERYBEAT_SCHEDULE entries from step 4 above to docker/conf/local.py. allianceauth_beat picks them up on its next restart:
docker compose restart allianceauth_beat
Same as steps 5-6 above - both are done from the Django admin site and your EVE SSO application's own config, not container config, so there's nothing docker-specific about either.
The backend is a django-ninja API (wh_mapper/api/) plus Django Channels websocket consumers (wh_mapper/consumers.py); the frontend is a React + TypeScript SPA in frontend/.
python manage.py test wh_mapper
or, via tox:
make tox_tests
The prebuilt frontend lives in wh_mapper/static/wh_mapper/. To rebuild it after making changes in frontend/src/:
cd frontend
npm install
npm run build
npm run dev starts a Vite dev server on port 3000, proxying /wh-mapper/api/ and /ws/wh-mapper/ to a Django dev server running on port 8000.
All bug fixes or features must not include extra superfluous formatting changes, if you want to reformat the entire repository put it in it own request.
All Contributions big and small are welcome, we ask that if you submit code you understand how it works.
Make sure you have signed the License Agreement by logging in at https://developers.eveonline.com before submitting any pull requests.
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 1.0.0b2 | 6 Oct 2026 | 8 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 17:07 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:126f8e5ca1c94b9dd01020049cd4b1e8500d35e69628003a08911d29d9eb6a98