Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
ValheimWebMap
Live web map for your dedicated server. 3D/2D view with the game's own models, fog of war, players, bases, portals, stats.
| Date uploaded | 3 days ago |
| Version | 2.1.6 |
| Download link | f00d4tehg0dz-ValheimWebMap-2.1.6.zip |
| Downloads | 134 |
| Dependency string | f00d4tehg0dz-ValheimWebMap-2.1.6 |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2350README
Valheim WebMap
Mod for your Valheim server. Makes a live map of your world you open in a
browser. Share http://your_ip:3000. Players install nothing. Server only.

What it does
- Big map, every metre. One pixel is one metre. Seven zoom levels, and three more past that where buildings, trees and rocks are drawn as shapes so they stay sharp. Shows ground the way players shaped it: flat bases, moats, roads, farms. Trees, bushes and rocks drawn on top as their own layer. Checkbox turns them off. Cut a forest down, map shows the clearing next time it draws.
- Buildings. Every piece anyone placed, drawn as a footprint in its material colour. Hover to see what it is.
- 3D view. One click. Real ground, water, and every object drawn with the game's own model: walls, roofs, portals, ruins, trees, rocks, boats. Same spot, same rotation, same size as in the game. Ground has fine grain up close, no seams, no grid. Base looks like your base.
- Sky, sun, shadows. The 3D view has a real sky: sun, stars, dawn and dusk colours, fog to the horizon. Lit by the server's clock, so night on the server is night on the map (or pick noon, evening... in Layers). Buildings and trees cast shadows. Water ripples and mirrors the sky. Shadows are off on phones unless you turn them on.
- Fog of war. Ground nobody walked on is black. No switch to turn it off. Close-up tiles only draw where players walked.
- Old trips count. Install the mod on a world you played for months and
the map still opens up everywhere anyone has been. The world save remembers
which zones the game built, and it only builds them where someone stood.
The mod lifts the fog there at start. Your in-game map itself lives in
your character file, which the server never sees, so this is the closest
thing to it. See
reveal_visited. - Live players. Arrow points where they look. Health bar. Biome. PvP, sleeping, dead. Follow one. Pings and chat pins land on the map.
- Player card. Click a player, on the map, in 3D, or in the list. Card shows health, stamina, eitr, what they wear and hold, how long they played, deaths, distance walked, visits. Updates live. Buttons: follow, go to, switch view.
- Markers. Portals with their tags and links, tombstones, player bases,
boats, carts, your own
markers.json. Boss altars, dungeons, traders and the world seed are never sent out. No spoilers. - Stats. Per player: time played, visits, deaths, distance, portal trips, biomes. Per server: day, explored %, pieces built, trees, who was online last 24 h.
- Events. Joins, leaves, deaths, chat, pings. Saved to
events.jsonl. - Export. Pick an area, get a 3D file. Ground, water, every building and object, trees, markers. Opens in Blender, Unreal, Unity, Godot. Unreal pack has a heightmap too. See docs/EXPORT.md.
- Share links. The address bar follows you:
#x,z,zoomin 2D,#x,z,zoom,3d,distance,heading,tiltin 3D. Copy it (link button) and the other person lands on the same spot, same camera. - Also: search, works on phone, dark UI, Discord webhook,
POST /announceto shout at everyone.

Install
Get the zip from GitHub Releases
(each release has a SHA256SUMS next to it, for people who pin things),
from Thunderstore, or from Nexus. Same zip everywhere.
-
Put BepInEx on the server. Unzip. Copy the
plugins/WebMapfolder to<server>/BepInEx/plugins/WebMapFolder holds
WebMap.dll,websocket-sharp.dll,web/. Mod managers (r2modman, Gale, Thunderstore) do this for you. Lost theweb/folder? No matter: a copy lives inside the DLL and the map still shows. Put aweb/folder next to the DLL and it wins, so you can change the page.Docker (lloesche/valheim-server): with
BEPINEX=true, BepInEx lives at/opt/valheim/bepinex/BepInExon the data volume./config/bepinexis only config. Plugin goes to/opt/valheim/bepinex/BepInEx/plugins/WebMap. Easiest: bind-mount a host folder there. Example intools/docker-compose.test.yml. Config lands in<config volume>/bepinex/com.valheimwebmap.server.cfg. Open the port:-p 3000:3000/tcp. -
Start server once. It writes the default config to
BepInEx/config. -
Edit config if you want. Restart. Config is read at start only.
-
Open port 3000. Visit
http://your_ip:3000.
Map data lives in plugins/WebMap/map_data/<World>/. Model library in
plugins/WebMap/map_data/models/. Keep both when you update.
Behind Cloudflare or another cache? Purge it after every update, or set a rule to not cache this host. The mod already sends the right headers.
First start
Server draws the zoomed-out map first (zoom 0–5, ~500 tiles, one to two minutes on one core). Then close-up tiles where players walked. Page works right away: missing tiles show the zoomed-out one blown up, swap in when done. Counter bottom-left: drawn / waiting.
First start also exports the 3D models. A couple of thousand prefabs, a few
minutes. Done once, kept forever. Log: WebMap: models exported N (readable meshes R, ...).
Textures
Server does not let anyone read textures. So the mod reads them straight out of the game's own files instead. Background thread, low priority, starts a few seconds after the first model export. Windows, Linux, Docker. Nothing to install. One to two minutes over ~2 GB of game files, once per game version. Log:
WebMap: extracting 142 textures from the game files in .../valheim_server_Data
WebMap: 139 of 142 textures extracted from 61 files in 74s, 3 not found
WebMap: 180 models to re-export with newly extracted textures
Textures are game files. Not in this repo. Not in the mod zip. Never shared.
They only ever sit in your map_data/models/ folder, made from your own
server. use_textures = false skips all of it (flat colours).
texture_max_size caps them (default 512).
Doing it by hand, same job, plain Python 3, no packages:
python3 tools/extract_textures.py <valheim_server_Data> <plugins>/WebMap/map_data/models
Meshes
Same story for the shapes. The engine locks most meshes (about seven in eight: carts, beehives, ruins, rocks, furniture...). Ask it for the vertices and it says no. Old versions drew those as boxes, or left parts out: a cart with boxes but no cart. Now the mod reads the locked meshes out of the game files too, right after the textures, and draws the real thing. Once per game version, a few minutes. Log:
WebMap: extracting 1900 meshes from the game files in .../valheim_server_Data
WebMap: 1880 of 1900 meshes extracted from 800 files in 180s, 20 not found
WebMap: 1700 models to re-export with newly extracted meshes
Mesh files live in map_data/models/meshes/. Game data, same rules as
textures: never in the repo, never in the zip, never served. Only the
finished .glb models go to the browser. extract_meshes = false turns it
off (locked meshes stay boxes). By hand:
python3 tools/extract_meshes.py <valheim_server_Data> <plugins>/WebMap/map_data/models
python3 tools/extract_meshes.py <valheim_server_Data> --dump Cart,beehive # look inside the game files
Still a box or a gap? The log names every mesh it could not find or decode. Send it in an issue.
Server load
Map drawing runs on its own thread (render_threads, default 1, low
priority). Game thread only does: one slow walk over the world every
sweep_interval seconds (3,000 objects per frame), player snapshot once a
second, fog, model export within export_ms_per_frame. PNGs are made by the
mod's own writer, off the game thread.
If the engine refuses to sample ground off the game thread, the mod does a
few rows per frame on it instead. Slower. Never freezes. render_threads = 0
forces that.
Disk: whole world at 1 m/px is ~6,400 tiles, few hundred MB. Only walked
ground draws close up, so a normal world is tens of MB. max_render_zoom = 6
cuts that by four.
Config
| Section | Key | Default | What |
|---|---|---|---|
| Render | render_threads |
1 | threads drawing tiles (0 = do it on game thread, slowly) |
| Render | prerender_zoom |
5 | draw the whole world up to this zoom at first start |
| Render | max_render_zoom |
7 | closest zoom over walked ground (7 = 1 m/px) |
| Render | height_max_zoom |
7 | closest zoom for height tiles (3D) |
| Sweep | sweep_interval |
120 | seconds between world walks |
| Sweep | zdos_per_frame |
3000 | objects looked at per frame during a walk |
| Markers | reveal_all |
false | no fog: whole world shown and sent out. Testing only |
| User | always_map |
true | lift fog where hidden players walk. Their spot stays hidden |
| User | always_visible |
false | ignore players' "hidden" setting |
| User | show_last_seen_position |
false | offline players' last spot in stats |
| Server | server_port |
3000 | HTTP port |
| Server | map_title |
(server name) | title on the page |
| Server | enable_3d |
true | offer the 3D view |
| Server | event_log |
true | write events to events.jsonl |
| Server | legacy_map |
true | build one big map.png for /map |
| Models | export_models |
true | export prefab meshes for 3D |
| Models | object_categories |
piece,other,rock,bush,tree | what the 3D view and export get |
| Models | use_textures |
true | textures on 3D models (off: flat colours) |
| Models | texture_max_size |
512 | longest texture edge |
| Models | export_ms_per_frame |
6 | game-thread ms per frame for export |
| Texture | explore_radius |
100 | metres revealed around a player |
| Texture | reveal_visited |
true | lift fog everywhere the world save shows players have been, even before the mod |
| Texture | reveal_visited_margin |
3 | how far in from the edge of the built zones the reveal stops (0 = 320 m, 3 = 150 m, 4 = 100 m) |
| Discord | discord_webhook, discord_invite_url |
webhook for events | |
| Server | webmap_url, max_pins_per_user |
link shown in game, pin limit | |
| User | web_pins |
true | let the web page place pins (right click / long press) |
| User | web_edit_bases |
true | let the web page rename or hide auto-detected bases (click the marker) |
| Markers | base_min_pieces |
30 | built pieces in a spot before it counts as a base |
| Markers | base_min_per_cell |
6 | pieces per 64 m cell before that cell joins a base |
| Models | extract_meshes |
true | read locked meshes out of the game files (else boxes) |
| Server | websocket_compression |
false | permessage-deflate on the live feed. Off: IIS ARR and some proxies drop every frame with it on |
Your own markers
Put markers.json next to the world's map data
(plugins/WebMap/map_data/<world>/markers.json). Read again when it changes.
{ "sets": [
{ "id": "mines", "label": "Mines", "markers": [
{ "x": 2210, "z": 980, "label": "Silver mine", "icon": "mine", "description": "north face" }
] }
] }
Icons: pin dot fire mine house cave boss trader dungeon camp village ruin runestone wreck portal tombstone boat cart poi spawn.
Player card
Click a player. Card pops up next to them. Health, stamina, eitr as bars. State: PvP, sleeping, dead. Biome and coordinates. What they hold in each hand and wear on head, chest, legs, back, belt. Lifetime numbers: played, deaths, walked, visits, portal trips. Follow, go to, switch 2D/3D. Card stays open and updates live. Escape or click away closes it. Works on the 2D map, in 3D, and from the Players list. Game only sends current stamina and eitr, not the max, so those bars fill against 100 or the current value.
Export a 3D scene
Download button, top bar. Pick the area (what you see on the 2D map, or 256 m to 1.5 km around the centre), ground detail, what to include, format:
- glTF, instanced. One mesh per prefab, placed many times. Small file. Blender, Godot, three.js.
- glTF, one object per node. Unreal, Unity, anything that lacks the instancing extension. Bigger file, same stuff.
- Unreal pack. Zip: the flat scene,
heightmap_r16.pngfor a Landscape,instances.csvandmarkers.csvin Unreal units, an editor Python script, README with the exact Landscape scale and location numbers.
Made in the browser from what the map already shows. Fog applies: nothing undiscovered leaves the server. Meshes and textures are the game's own files from your server. Use the export yourself, do not pass it around. Steps per editor in docs/EXPORT.md.
HTTP API
| Path | Returns |
|---|---|
/tiles/map/{z}/{x}/{y}.png |
ground tile, zoom 0–7. 404 + X-WebMap-Tile: pending while drawing |
/tiles/veg/{z}/{x}/{y}.png |
tree and rock overlay, zoom 5–7, transparent PNG |
/tiles/height/{z}/{x}/{y}.png |
height tile, Terrarium encoding |
/data/structures/index.json, /data/structures/{cx}_{cz}.json |
pieces per 256 m chunk |
/data/veg/{cx}_{cz}.bin |
vegetation points per chunk (Vegetation.cs) |
/data/objects/index.json, /data/objects/{cx}_{cz}.bin |
every placed object per chunk: prefab, position, rotation, scale (WorldObjects.cs) |
/data/prefabs.json |
model library: which prefabs have a model, bounds, triangles, textures |
/models/{hash}.glb, /models/tex_*.png |
exported models and textures. Cacheable |
/data/markers.json |
marker sets |
/data/players.json |
live players: position, health, stamina, eitr, gear, state |
/data/stats.json, /data/events.json, /data/pins.json |
stats, events, pins |
/data/fog.png |
explored mask, north up |
/config, /api/status |
config, drawing and sweep status |
POST /api/sweep |
walk the world now |
POST /api/rerender?zoom=N |
redraw tiles from zoom N up (token) |
POST /api/reexport |
export every model again (token) |
POST /api/base |
JSON {x, z, label} renames the base nearest that spot, {x, z, hidden: true} hides it, {x, z} goes back to auto name. Open to everyone unless web_edit_bases = false (then token) |
POST /api/bases/reset |
forget every rename and hide (token) |
POST /api/reload |
forget cached web files, refresh every open browser (token). Swap files in web/ without restarting the game |
POST /announce |
message on every player's screen (token) |
/map, /map.jpg, /fog, /players, /pins, /messages, /structures, /structures/stats, /structures/refresh, /forest, /forest/stats, /vehicles |
simple endpoints: one-image map, plain lists |
Websocket at /ws. JSON frames: hello, players, events, tiles,
world, ping, pin, rmpin, reload.
Tile grid: world is a 20,480 m square around the origin. Zoom 7: one pixel
is one metre. Tile (0,0) is the north-west corner. Zoom z has 2^(7-z)
metres per pixel. tx = floor((x + 10240) / (256 · 2^(7-z))),
ty = floor((10240 − z_world) / (256 · 2^(7-z))).
Token
POST /announce, /api/rerender, /api/reexport, /api/reload need a secret in header
X-Announce-Token. Secret is read from file announce.token next to the
DLL. No file, no access.
New web files, no restart
Changed something in web/? Copy the files over, then:
curl -X POST -H "X-Announce-Token: yoursecret" http://localhost:3000/api/reload
Server forgets its cached copies and every open browser reloads. Only the
web files. A new DLL still needs a game restart. For dev work set
cache_server_files = false and the server reads from disk every time.
Caches (Cloudflare and friends)
Every script and stylesheet URL carries a hash of the file (app.js?v=62e6…),
written into the page by the server. New file, new URL. So a CDN or a
browser can never mix an old view3d.js with a new app.js: one hard
refresh of the page and everything matches. Only index.html itself must
not be cached for hours. If you put Cloudflare in front, tell it to bypass
cache for / and /index.html (or purge after each update).
Pins
From the web page. Right click the map (long press on a phone). Pick a
type, write a label, add pin. Your own pins have a "Remove pin" button.
Only the browser that made a pin can remove it. web_pins = false turns
this off. max_pins_per_user caps pins per browser, old ones go first.
Bases. Bases are found by looking at what is built. Click a base marker
to give it a name, hide it, or go back to the auto name (portal tag near it,
else "Base"). Saved in map_data/<world>/bases.json, so a re-scan keeps
your names. web_edit_bases = false leaves that to the token holder. If a
lone workbench shows up as a base, raise base_min_pieces.
From chat. Type in game:
!pin [type] [text]— typesdot,fire,mine,house,cave!undoPin,!deletePin [text]
Chat pins have a catch. Since Valheim 1.0 the game sends chat straight from
player to player, not to the server. The server only sees chat while it
passes it on between two or more players. Alone on the server, your !pin
never arrives. Not a bug in the mod, nothing the mod can do. Use the web
page, it always works. Pings (middle click on the in-game map) still reach
the server, alone or not.
Build
Windows: .\build.ps1. Needs .NET SDK and the Steam "Valheim Dedicated
Server" tool, or -ValheimManaged <path>. Linux/macOS: ./build.sh.
Output: dist/ValheimWebMap-<version>.zip and dist/pkg/plugins/WebMap/.
-Deploy <plugins dir> or --deploy copies the plugin there.
Releases are built by GitHub Actions. Bump version_number in
manifest.json and VERSION in WebMap/WebMap.cs, add the changelog
section, then tag: git tag v2.1.5 && git push origin main v2.1.5. The
workflow fetches the dedicated server with SteamCMD, runs build.sh, and
attaches the zip, a SHA256SUMS, and the changelog section to the release.
Public demo site
Want a demo people can click on without showing your real world? Run the mock server in Docker. Made-up island, fake players walking in circles, fake chat. No game files inside, nothing to leak, nothing it can write.
docker compose -f tools/docker-compose.demo.yml up -d --build
Port 3000. Put your reverse proxy or Cloudflare Tunnel in front on a demo
hostname. Title and world name come from WEBMAP_TITLE and WEBMAP_WORLD
in the compose file. Image is ~60 MB, uses under 100 MB RAM. Tiles are drawn
on first request and cached; the demo loops forever.
Your real map: keep it for your players. Cloudflare Access, a password on the proxy, or a hostname nobody guesses. The mod itself has no login.
Try it without a real server
tools/mockserver.js (Node, no packages): serves the web app with a made-up
world, fake players, fake events. node tools/mockserver.js, open
http://localhost:3000.
tools/docker-compose.test.yml: real dedicated server (lloesche image)
with a copy of your world.
# copy world folder to valheim-test\config\worlds_local\<World>\
.\build.ps1 -Deploy valheim-test\plugins
docker compose -f tools\docker-compose.test.yml up # first run downloads server
Then http://localhost:3000. In game: Join by IP, 127.0.0.1:2456
A copied world lifts the fog where players have been (reveal_visited), not
what each player's in-game map shows: that lives in their character file.
Want it all: set reveal_all = true in
valheim-test\config\bepinex\com.valheimwebmap.server.cfg and restart. Tiles
still draw as you look.
Licence
MIT. See LICENSE. Leaflet (BSD-2) and three.js (MIT) vendored under
web/vendor.
CHANGELOG
Changelog
2.1.6
- Rename or hide the auto-detected bases from the web page. Click a base marker: type a name and Rename, "Hide this base" to drop it, "Auto name" to go back to the portal tag. Saved in
map_data/<world>/bases.json, kept across re-scans.web_edit_basesturns it off (a token still works).POST /api/base,POST /api/bases/reset. (#14) - 3D view links. The URL now follows the 3D camera (
#x,z,zoom,3d,distance,heading,tilt), so a copied link opens the same spot, same angle. Before, only 2D updated the URL. (#13) - Sharp 2D close-ups. Past the tiles' native zoom (1 m per pixel) the map used to scale pixels up into blocks. Now trees and rocks are drawn as shapes from the vegetation data, buildings are drawn at the screen's own resolution (crisp on phones and Retina), and the ground is scaled smoothly instead of in blocks. (#12)
base_min_piecesandbase_min_per_cellconfig: how much built stuff it takes before a spot counts as a base. Raise them if a lone workbench shows up as one.
2.1.5
- GitHub Releases. A version tag builds the mod in GitHub Actions and attaches the zip plus
SHA256SUMSto a release, so the mod can be pinned by hash. (#15) - Script and stylesheet URLs carry a hash of the file (via the import map), so a CDN or browser cache can never mix old and new web files. Fixes "WASD works in 2D but not 3D" / "time of day does nothing" after an update behind Cloudflare.
- Fix: a few box-only prefabs were re-exported every minute ("N models to re-export with newly extracted textures" forever), which also kept the texture extractor from starting for anything new.
- Keyboard everywhere: WASD/arrows move, Q E turn, R F tilt, Z X zoom, Shift fast, P follow next player, M 2D/3D, L layers, Home spawn. Double click in 3D centres on that spot. README "Controls".
- Follow works in 3D: camera glides after the player; drag, WASD or Esc lets go. Following carries over when you switch 2D/3D.
- "Lighting (3D)" heading in Layers for time of day and shadows.
2.1.4
- Sky, sun, moon and shadows in 3D. Sky dome with sun disc, stars, dawn and dusk colours; fog to the horizon; the sun follows the server's time of day (Layers > "Time of day", or pick a fixed one); buildings, trees and hills cast shadows (toggle, off on phones by default); water ripples and mirrors the sky; filmic tone mapping.
2.1.3
- Every model, not one in eight. The engine locks most meshes, so the 3D view drew boxes (a cart with its boxes but no cart, a bare beehive). The mod now reads locked meshes out of the game files like it already did textures:
extract_meshesconfig,map_data/models/meshes/,tools/extract_meshes.pyby hand. Model library format 5: everything is exported again once. - Materials that keep their colour map under
_BaseMapand friends get their texture too.
2.1.1
- Addressed issue #2 with !pin command not working. Pins from the web page. Right click (long press on a phone), pick a type, label, done. Remove your own from the popup.
POST /api/pin,POST /api/unpin?id=.web_pinsturns it off. - Addressed issue #3 with title bar in Mobile not being supported.
- Addressed issue #1 with Web dir not being included in build, causing mod manager Gale to fail installation
- Addressed issue #5 with @Aughen PR #6. WebSocket compression off by default,
websocket_compressionturns it back on. - Addressed issue with fog coverage not being 100% on initial load with @clanofartisans PR #7
- Build script: one
-ValheimManagedpath works. (#6) POST /api/reload(token): Update the web app without restarting the game.- Layer list says "Pins", not "Chat pins".
2.1.0
- Old trips count. Fog lifts everywhere players have already been, even from
before the mod was installed. The world save lists every zone the game
built, and it only builds them near a player. Runs at start and once a
minute.
reveal_visited(on) andreveal_visited_margin(3 zones, about 150 m from where they walked). - Player card. Click a player on the map, in 3D or in the list: health, stamina, eitr, what they wear and hold, state, lifetime stats. Updates live. Server now sends stamina, eitr and gear.
- Export. Pick an area, get a 3D scene: glTF (instanced, or one node per object) or an Unreal pack with a 16-bit heightmap, CSVs in Unreal units and an editor script. Made in the browser. Fog applies.
- Demo site.
tools/Dockerfile.demoanddocker-compose.demo.ymlrun the mock server as a public demo. Mock draws the tree and rock overlay and loops forever. - Stats table fits the sidebar. README in plain words.
1.0.1
First public release. Same as 1.0.0 plus README and screenshot fixes.
1.0.0
First release.
- Tiled map, seven zoom levels, 1 m/px. Rendered from world generator plus player terraforming (levelled ground, moats, paved roads, farmland). Trees, bushes, rocks as a separate overlay layer. Close-zoom tiles render only over explored ground. Re-render when ground changes. Worker thread, own PNG encoder, sliced main-thread fallback.
- Buildings as vector data per 256 m chunk: footprint, height, material, prefab. Drawn as material-coloured footprints with hover info.
- 3D view (three.js). Terrain from height tiles with quadtree LOD, seamless
between tiles, clean ground tiles with tiled fine grain up close. Water.
Players. Markers. World objects as game's own meshes: mod exports each
prefab to glTF once (
map_data/models/), publishes each chunk's objects with prefab, position, rotation, scale. Browser instances models. - Textures pulled from the game's own asset files by the mod itself, in the
background, on every platform.
tools/extract_textures.pyas manual fallback.POST /api/reexportrebuilds models. - Fog of war always on. Black over unexplored ground. World locations (bosses, dungeons, traders) never published.
- Markers: portals with tags and links, tombstones,
player bases (clusters of built pieces), boats, carts, custom
markers.jsonsets. - Layer toggles drive 2D and 3D: buildings with opacity, players, chat pins, trees and rocks overlay (2D), labels, 256 m grid, marker sets, object categories.
- Stats per player: playtime, sessions, deaths, distance, portal trips, biomes. Per server: day, explored %, counts, online history.
- Event feed with
events.jsonlhistory. Deaths carry position. - Web app: dark UI, sidebar, search, permalinks, follow mode, mobile layout. No build step.
- Simple endpoints kept for scripts:
/map,/players,/pins,/messages,/structures,/forest,/vehicles. Websocket speaks JSON. - Discord webhook,
POST /announce, chat pin commands.