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:

HTTP
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:

  1. Country — list match against countryCode in the configured mode (ALLOWLIST / BLOCKLIST).
  2. ASN — same logic for AS numbers.
  3. 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

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.