How it works
Three moving parts: your server, the Worker, and the upstream provider. The data path is short.
The short version
Player connects. Plugin checks geo_cache.json on disk. If there's a fresh entry, the rules run locally — no network. If there isn't, the plugin sends one HTTPS request to the Worker. The Worker queries the geolocation provider, returns the result, the plugin caches it, and evaluates.
Detailed flow
1. Plugin receives a login
The plugin listens to the platform's pre-login event. On Bukkit that's AsyncPlayerPreLoginEvent. On BungeeCord it's LoginEvent. On Velocity it's LoginEvent with the async continuation API. The event gives us the connection IP, the player's name, and (sometimes) their UUID. The plugin then submits the IP to its internal GeoRestrictService.checkIp(...) on a background thread.
2. Cache check
The cache is a ConcurrentHashMap in memory, mirrored to geo_cache.json on disk via a debounced async save (5s window). Saves are atomic: write to .tmp, then Files.move(... ATOMIC_MOVE), with a non-atomic fallback for filesystems that don't support it.
A cache hit short-circuits the entire network path. The cached GeoResponse is fed straight into the rule evaluator. A cache miss goes to step 3.
3. Worker query
Single HTTPS request:
GET /?ip=1.2.3.4 HTTP/1.1
Host: geoprotect.demonzdevelopment-e64.workers.dev
User-Agent: GeoRestrict/2.0.0
Authorization: Bearer <token> # only if gatewayToken is set
The plugin hands the IP, the timeout from connectionTimeoutMs, and the optional bearer token. The Worker is the only thing that knows your provider credentials — the plugin never talks to the upstream geolocation providers directly. That's the Worker's job. If you self-host a VPS gateway, the Worker will talk to your VPS first and only reach the upstream providers as a fallback.
4. Worker → provider
Inside the Worker, the provider order from PROVIDER_ORDER is walked. For each provider, the Worker picks a random endpoint and (if configured) a random key from the pool. The first non-error response wins. Results are stored in caches.default for CACHE_TTL_SECONDS (default 86400s = 24h).
If every provider fails, the Worker returns HTTP 502 with an all_providers_failed body. The plugin treats this as a lookup failure and runs the lookup-failure policy.
5. Rule evaluation
Three rules, in order. First match wins:
- Country — list match against
countryCodein the configured mode (ALLOWLIST / BLOCKLIST). - ASN — same logic for AS numbers.
- VPN — boolean flags (
isVpn,isHosting,isProxy) plus a case-insensitive keyword scan of the AS name.
6. Result
If a rule matches, the event is denied with the configured kick message. If nothing matches, the player proceeds normally. In both cases the Discord webhook fires if configured.
Privacy boundary
On a cache miss, the player's IP leaves the server for the configured gateway. The gateway forwards it to a lookup provider and returns normalized country, ASN and proxy data. Each provider has its own logging and retention policy. Locally, the plugin writes its config and cache; the optional update checker makes a Modrinth API request but does not write a separate tracking file. Set CACHE_TTL_SECONDS=0 on your Worker if you do not want edge caching.
Caching details
- Cache file:
plugins/GeoRestrict/geo_cache.jsonon the server where the plugin runs. - Loaded on startup, written on a 5s debounce after the last change, written synchronously on shutdown.
- Old entries are purged every 6h via a scheduled task (configurable TTL).
- Hard cap of
maxCacheEntries; oldest entries are evicted in order when exceeded. - On load, entries older than 365 days are dropped silently — that's your upper bound for "stale data after a long offline period".
Folia-specific note
The plugin detects Folia at startup (Class.forName("io.papermc.paper.threadedregions.RegionizedServer")) and switches to a Folia scheduler adapter. The adapter uses the global region scheduler for sync tasks and the async scheduler for everything else. The plugin itself doesn't touch world state, so region locality only matters for the configuration-watcher task, which is fine to run on the global region.