Blog article (in french https://iooner.io/hswd/)
A live, interactive globe of hackerspaces around the world.
hswd.iooner.io β crafted with β€οΈ in π§πͺ by iooner @ LiΓ¨ge Hackerspace
A real-time map aggregating hackerspaces from two sources:
- SpaceAPI β an open standard that hackerspaces use to publish their live status (open/closed/limited). Updated every few hours via a cron job.
- mapall.space / wiki.hackerspaces.org β static data for spaces without a SpaceAPI endpoint, shown as blue dots.
Nearby spaces cluster together at low zoom levels. Click a cluster to expand it, click a dot to see details.
| Color | Meaning |
|---|---|
| π’ Green | Currently open (SpaceAPI live) |
| π‘ Yellow | Open to members only β open_to_visitors: false (proposed for SpaceAPI schema v16) |
| π΄ Red | Currently closed (SpaceAPI live) |
| βͺ White | API temporarily unavailable β space kept visible for 30 days |
| π΅ Blue | Static data from wiki / mapall, no live status |
| π Orange (footer) | API unreachable for 30+ days β removed from the globe |
Two ways to get listed:
- SpaceAPI β implement the SpaceAPI standard and submit your endpoint to the directory. You'll get a live colored dot.
- Wiki β register on wiki.hackerspaces.org following their guidelines. You'll appear as a static blue dot.
Changes appear on the next cache update (every 10 minutes).
Coordinates and info come directly from the sources above β fix them at the source (your SpaceAPI endpoint or your wiki page) and the globe will update automatically on the next run.
.
βββ index.html # Frontend β MapLibre GL JS globe, single file
βββ api.php # API endpoint (see below)
βββ assets/
β βββ countries-50m-hswd.json # World atlas 50m, pre-processed (antimeridian fix)
β βββ countries-10m-hswd.json # World atlas 10m, pre-processed (antimeridian fix)
β βββ imin.gif # Rick & Morty asset, very important
βββ cache/
βββ update_cache.php # Cache pipeline (run via cron)
βββ hackerspaces_cache.json # Generated β main data cache
βββ run_history.json # Generated β pipeline run history (7 days)
βββ banlist.json # Manual exclusion list
βββ test_geojson.json # Dev snapshot β ignored in production
βββ .htaccess # Blocks direct access to cache files
Note:
hackerspaces_cache.json,run_history.json, andtest_geojson.jsonare generated at runtime and should be added to.gitignore.
countries-50m-hswd.jsonandcountries-10m-hswd.jsonare pre-processed from world-atlas v2.0.2 to fix antimeridian artifacts (Russia, Fiji, Antarctica) in MapLibre's globe projection. They are committed to avoid requiring a Node.js build step on deploy.
api.php serves the cache. All endpoints return JSON with CORS headers (Access-Control-Allow-Origin: *).
Returns a GeoJSON FeatureCollection β the format consumed by the frontend.
Query parameters:
| Parameter | Values | Description |
|---|---|---|
format |
geojson |
GeoJSON FeatureCollection |
format |
history |
Pipeline run history |
state |
open limited closed unknown static |
Filter by state |
limit |
integer β€ 1008 | Max runs to return (history only) |
GeoJSON response:
{
"type": "FeatureCollection",
"metadata": {
"last_update": "2026-06-13T16:20:02+00:00",
"stats": {
"open": 73, "limited": 1, "closed": 123,
"unknown": 3, "static": 387, "down": 16,
"expired": 0, "banned": 1, "no_coords": 2
},
"count": 587,
"cache_age_hours": 0.1,
"cache_age_text": "6 minutes ago"
},
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [5.5856, 50.6427] },
"properties": {
"name": "Liège Hackerspace",
"state": "limited",
"city": "Liège",
"country": "",
"address": "Rue de la Loi 16, 4020 Liège, Belgique",
"message": "Ouvert aux membres uniquement.",
"url": "https://lghs.be",
"logo": "https://raw.githubusercontent.com/LgHS/branding/...",
"lastchange": 1718290800,
"last_seen": "2026-06-13T16:20:02+00:00"
}
}
]
}Coordinates: GeoJSON order is
[longitude, latitude].
Returns the pipeline run history for the stats modal.
{
"runs": [
{
"ts": "2026-06-13T16:20:02+00:00",
"dur": 73,
"stats": { "open": 73, "limited": 1, "closed": 123, ... },
"down": ["Reaktor 23", "Leitstelle511", ...],
"expired": [{ "name": "OldSpace", "days": 45 }],
"unknown": [{ "name": "FabLab AllgΓ€u", "days": 0, "last_seen": "..." }],
"banned": [{ "name": "NSHkr", "reason": "...", "source": "mapall" }],
"total": 587
}
]
}Returns the raw cache JSON. Kept for backward compatibility.
cache/update_cache.php β run via cron (every 10 minutes at hswd.iooner.io).
Pipeline stages:
- Download the SpaceAPI directory (~246 spaces)
- Load the existing cache (for grace period logic)
- Fetch all SpaceAPI endpoints in parallel (10 concurrent curl requests, 5s timeout)
open: true+open_to_visitors: falseβ statelimited- API down + cached < 30 days β state
unknown(kept on globe) - API down + cached β₯ 30 days β
expired(removed) - API down + never cached β Nominatim geocoding fallback β state
staticordown
- Merge with mapall.space/wiki.json (~470 spaces) β deduplication by name similarity and geographic distance (< 1 km)
- Write
hackerspaces_cache.json+ append torun_history.json
Configuration (top of update_cache.php):
| Variable | Default | Description |
|---|---|---|
$timeout |
5 |
Per-request curl timeout (seconds) |
$maxConcurrent |
10 |
Parallel curl requests |
$expirationDays |
30 |
Days before a silent space is removed |
Run manually (browser or CLI):
# CLI
php cache/update_cache.php
# Browser β readable output with live progress
https://yourdomain.com/cache/update_cache.phpThe script outputs
text/plainwith live progress when called from a browser (output buffering disabled).
cache/banlist.json β manually maintained exclusion list. Checked before any space enters the cache.
{
"_comment": "Two levels: spaces (full exclusion) and domains (URL hidden, space kept if coords known).",
"spaces": [
{
"name": "ExactSpaceName",
"reason": "Why it was banned",
"since": "2026-06"
}
],
"domains": [
{
"domain": "example.com",
"reason": "Domain squatted",
"since": "2026-06"
}
]
}The name must match exactly what appears in the SpaceAPI directory or mapall (case-sensitive). Check the cron output logs to find the exact name.
Think a ban should be lifted? Open an issue or PR.
| Layer | Tech |
|---|---|
| Globe | MapLibre GL JS v5.24 β globe projection |
| Map data | world-atlas v2.0.2 (pre-processed, antimeridian-fixed) |
| Clustering | MapLibre native cluster: true on GeoJSON source |
| Frontend | Vanilla JS + CSS, single index.html, no build step |
| Backend | PHP 8+ |
| Data sources | SpaceAPI directory + mapall.space/wiki.json |
| Geocoding | OpenStreetMap Nominatim (fallback only) |
| Fonts | IBM Plex Mono (Google Fonts) |
No build step required. Clone and serve:
git clone https://github.com/iooner/Hackerspaces-World-Domination.git
cd Hackerspaces-World-Domination
# PHP built-in server
php -S localhost:8080
# Then open http://localhost:8080
# The frontend falls back to cache/test_geojson.json if api.php is unavailableTo generate a fresh cache:
php cache/update_cache.phpIssues, PRs and feature requests are welcome at github.com/iooner/Hackerspaces-World-Domination.
Ideas and planned upgrades β contributions welcome!
- Search with fly-to β search field in the terminal prompt block, autocomplete over all space names, globe flies to the result on select
- Clickable status bar β clicking a segment (
open,limited,closedβ¦) in the footer filters the globe directly, as a natural alternative to the [ALL] / [OPEN] buttons - Shareable URL β
#space=Liege+Hackerspaceopens the globe centered on a space with the info card expanded; useful for QR codes on hackerspace doors - Kiosk mode β
?kioskquery param: UI hidden, continuous rotation, fullscreen; designed for wall-mounted screens at hackerspaces - Live tab title β
(59π’) HSWDβ updates the browser tab with the current open count
- Linked spaces β display
linked_spacesfrom SpaceAPI (e.g. LgHS) in the info card, with live status for each linked space and reverse links - Nominatim investigation β the Nominatim geocoding fallback currently fails 100% of the time server-side (~16 spaces lost as a result); needs a
curltest in SSH to determine if the hosting provider blocks outbound requests to OSM - Dead code cleanup β remove
test_geojson.jsonfrom the repo (already in.gitignore, needs agit rm --cached)
- Fix run history endpoint β
api.php?format=historyreturns "No history yet" despiterun_history.jsonexisting; path resolution issue betweenapi.php(__DIR__) andcache/to investigate