Skip to content
Nam ars nobilis via amoris est.
Apply

We are not safe until everyone is safe.

Public art — live and ongoing

Hate, watching itself get watched.

An autonomous agent reads public feeds around the clock — Bluesky, Mastodon — looking for the language of arson, incitement, banned symbols, far-right marches, racism, antisemitism, Islamophobia, and other hate incidents, worldwide. Every hit lands on the map below within minutes, color-coded by category, evidence attached, and marked exactly what it is: an unverified lead, never a verdict. Nothing gets deleted as it ages either — old reports just fade, still there, still counted. Anyone can add a report by hand, too — right-click or press and hold anywhere on the map to pin one. It never stops moving.

Why this exists

This started with what happened to me.

This is my public art piece, made after being harassed by neo-Nazi groups in Germany — and after seven years of racist acts directed at me personally, not as a footnote, as the actual reason this exists. I'm not interested in staying quiet about it, and I'm not interested in just complaining either. This is the fight, done in the only language I've spent twenty years getting good at: making something hard to see impossible to ignore. Discrimination Map is a public, interactive art piece before it's anything else — my answer to hate and violence, built to outlast whatever headline cycle it started in, and built for everyone this happens to, not just me.


Live data, embedded directly — use the map above to explore reports or file one yourself.


Overview

Discrimination Map aggregates, classifies, and plots reports of hate incidents worldwide on an interactive map. A background agent scans Bluesky and Mastodon (YouTube optionally) for public posts matching known indicators, flags a possibly-applicable statute where one exists, and stores each as an unverified lead pending review. Anyone can also file a report by hand — right-click or press-and-hold a spot on the map to pin one directly.

Why it exists

Hate incidents are scattered across social media, local reporting, and word of mouth, making patterns hard to see and hard to act on. Discrimination Map brings this into one place — every automated lead is evidence-linked and clearly flagged as unverified, and nothing reaches the public map without either real evidence or human review first.


Categories tracked

Reports are sorted into one of nineteen categories, each with its own color on the live map above — the original ten map to real Strafgesetzbuch statutes (still shown on the map's own Laws tab), the rest cover the broader, global taxonomy the map runs on today. Click a category in the map's legend to isolate it — matching markers connect into a colored zone, so a region's pattern becomes visible at a glance. The palette below is that same legend.

Arson / firebombingPhysical attackThreat against personsBanned symbol / saluteHolocaust denialIncitement to hatredBanned propagandaBanned organization activityFar-right assembly / marchFar-right context (needs review)RacismIslamophobiaAntisemitismHomophobia / transphobiaNeo-Nazi activity / signsXenophobiaSexual violenceHarassmentOther

Ethical & design principles

The rules the platform doesn't break, even for a good demo.

Leads, not verdicts

Every automated mark is stored as an "unverified lead." The classifier only suggests a possibly-applicable statute from the plain text of a public post — it never asserts guilt. Falsely accusing someone is itself an offence under German law (§186/§187 StGB), and the whole pipeline is built around that fact.

Incidents, not people

The platform documents events and already-public reporting — it is not, and must not become, a database of private individuals.

Self-check, human-gated

Every heartbeat also runs the test suite, a security audit, and per-source health tracking. Findings get logged for a human to act on — the agent never rewrites or redeploys itself.

Reviewed before it ships

Changes are developed and reviewed on a separate machine first. The server only ever runs a git pull and a service restart — never an unreviewed push straight to production.


How it works
Backend

Python + FastAPI + uvicorn — one process serves the API, the static frontend, and runs the monitoring agent as an asyncio background task. SQLite in WAL mode for storage.

Frontend

A single index.html — Leaflet.js with CARTO's light-mode tiles on OpenStreetMap data, no build step. Five tabs: Map, Incidents, Laws, Awareness, and Volunteer. Markers are colored per-category via per-category Leaflet layer groups, so the legend doubles as a live filter with zero extra requests. Right-click (or press-and-hold on mobile) any spot on the map to pin a report there directly.

Data flow

Agent scans a source → lawref.classify() matches text against statute/keyword indicators → a match becomes a row with category, law code (where applicable), and coordinates → GET /api/reports?limit=1000&all=true feeds the map, GET /api/categories feeds the color legend, GET /api/laws feeds the in-app Laws tab and report-form dropdown.

Sources

Bluesky and Mastodon work with zero configuration and are the reliable default sources today; YouTube switches on with a free Data API key. Reddit's code path exists but its anonymous endpoints are blocked from most cloud IPs as of this build, so it isn't a default source right now.

Classification

Matches text against real Strafgesetzbuch statutes — §86a (banned symbols), §130 (incitement), and others — plus a broader global taxonomy (racism, antisemitism, Islamophobia, xenophobia, homophobia/transphobia, and more), as possibly-applicable indicators for human review, never a verdict. Anyone can also propose a symbol or code that isn't in the awareness guide yet, straight from the map's Awareness tab.

Aging & visibility

Reports never get deleted as they age — after ten years a marker fades to a desaturated grey, still on the map, still counted. New reports (under 48 hours) pulse instead. Nothing reaches the public map unverified unless it carries real evidence or a moderator has reviewed it, and sensitive categories are always location-fuzzed for anyone who isn't a signed-in reviewer.

Geolocation

A built-in gazetteer of cities first, falling back to OSM Nominatim (rate-limited to 1 request/second) — global by default, not restricted to any one country.

Deployment

Docker on a VPS, behind Caddy as a shared reverse proxy (also fronting nabilvs.com and a client site on the same box) with Cloudflare in front of that. The case-study page you're reading embeds the live map directly via a scoped CSP frame-ancestors rule — one trusted origin allowed, everyone else still blocked. Per-IP rate limiting on report/apply/login endpoints, security headers on every response.


Symbol hunters

Banned symbols show up as graffiti and stickers constantly. Almost none of it gets documented.

Swastikas, SS runes, "88"/"18" codes, the Reichskriegsflagge, banned-party logos — public, photographable, illegal to display under StGB §86a, and mostly invisible to any kind of record. This is the one category anyone can document just by paying attention on a walk. The live map already has a guided flow for it: open it, hit "Spot a banned symbol?", and the report form comes up with the right category pre-selected.

What counts

Graffiti, stickers, or posters displaying a banned symbol, visible from public space — street furniture, walls, lamp posts, transit stops.

Staying safe

Photograph from public space, never confront whoever put it there, and don't remove or deface it yourself — report it to the property owner or city and leave the evidence intact.

Filing it

A photo link as evidence makes it far more useful for review. Reports are public, anonymous, and logged as unverified pending review — never a verdict.


Volunteer

This is a one-person project. It doesn't have to stay that way.

The pipeline, the classifier, the self-check loop — all of it is documented above, on purpose: this isn't a black box someone has to reverse-engineer to contribute to. Wherever you'd fit — adding a source, sharpening the classifier, translating docs, or eventually helping moderate — say so.

Code is open source on GitHub

What's next

The global, multi-category expansion is live now — server-side location fuzzing for sensitive categories and a human moderation queue both shipped as hard requirements before any of it went public, not afterthoughts. What's still ahead: more sources beyond Bluesky/Mastodon/YouTube, a bigger volunteer moderation team, and translating the categories/laws reference more deeply into languages beyond the four (English, German, French, Arabic) it already ships with.

Status

Live, global, and multi-category: a working FastAPI backend, SQLite persistence, Bluesky/Mastodon ingestion, a statute-and-taxonomy classifier, evidence-gated publishing with a human moderation queue, sensitive-category location fuzzing, and a self-check loop covering tests, security, and per-source health. Applications to volunteer, translate, code, or partner as an organization go through this page, with email verification and a Cloudflare-protected form.

Interested in partnering, volunteering, or learning more? Get in touch.

    Discrimination Map — NabilVS