Per-channel storefront geo-restriction for Vendure. Allow / block by country, by curated region preset (37 of them: EU, EEA, Schengen, GCC, ANZ, NATO, Commonwealth, …), by ISO-3166-2 subdivision (US states, CA provinces, AU states, DE Länder, IT regions, FR regions, ES autonomous communities, IN states, BR units, MX entities, UK constituent countries). Soft-block mode, IP allowlist, audit log, simulator and maintenance windows.
New in 0.7: SEO-safe bot allowlist so Googlebot / Bingbot / social
crawlers bypass by default · per-channel business-hours schedule
(overnight windows + DST handled) · one-line storefront drop-in JS
(hulo-geo.js) · branded block page (/geo-block/blocked).
Maintained by Wayne Garrison.
7-day free trial then £9.95/month, or £199 one-off lifetime at elite.charity/licence/buy/vendure-plugin-geo-block.
Add GeoBlockPlugin.uiExtensions to your compileUiExtensions config.
One-click bundles in five groups:
GET /geo-block/presets returns the live catalogue with country counts
and descriptions.
Each Vendure channel gets its own rules — perfect for multi-storefront installs (UK-only channel + EU channel + LATAM channel from one Vendure instance).
Per-channel mode field. block hides the storefront entirely;
soft renders it with a "we don't ship to your country" banner and
hides the checkout button. The verdict mode field is returned on
/geo-block/check so the storefront knows how to render.
JSON map { "US": ["CA","NY"], "DE": ["BY"] } enforced on visitors via
the channel custom field geoBlockAllowedSubdivisions. Catalogue at
GET /geo-block/subdivisions covers 11 countries (200+ subdivisions).
Legacy GB-only allowedGbRegions is preserved for back-compat.
Per-channel list of IPs / ranges that bypass every rule. Use for offices, oncall, payment-processor probes, monitoring.
Every block decision recorded in geo_block_event with country, region,
IP (hashed by default), UA, channel and reason. Admin Stats panel
shows top blocked countries, daily series and reason breakdown.
POST /geo-block/admin/simulate dry-runs a hypothetical visitor against
current rules without persisting anything — try any country / UK
region / IP and see exactly what would happen.
Per-channel blockMessage, blockRedirectUrl, blockLogoUrl. Falls
back to sensible defaults per block reason.
Reads cf-ipcountry / Akamai / Fastly region headers when present.
Saves a MaxMind lookup per request.
signingSecret HMAC-gates the ?country= override on /check so
storefront staff (or attackers) can't spoof a location at will.| Method | Path | Auth | Purpose |
|---|---|---|---|
GET | /geo-block/site-config | public | resolved channel rules (cache client-side) |
GET | /geo-block/check | public | per-request decision + reason (logs to audit) |
GET | /geo-block/presets | public | 37-preset catalogue |
GET | /geo-block/subdivisions | public | subdivision catalogue (11 countries) |
GET | /geo-block/admin/channels | admin | list channels + rules |
POST | /geo-block/admin/save | admin | save a channel's rules |
GET | /geo-block/admin/stats | admin | block totals + top countries + series |
POST | /geo-block/admin/simulate | admin | dry-run a hypothetical visitor |
POST | /geo-block/admin/gc | admin | prune old audit rows |
GET | /geo-block/status | admin | version + update status |
GET | /geo-block/hulo-geo.js | public | storefront drop-in helper JS |
GET | /geo-block/blocked | public | branded block page (HTML or JSON) |
On DOMContentLoaded the helper calls /geo-block/check, and:
mode=block → redirects to /geo-block/blocked (or the channel's
blockRedirectUrl if set, or the tag's data-redirect)mode=soft → injects a sticky top banner with the operator's messageOptional data-* attributes: data-redirect="/somewhere-else",
data-timeout-ms="2500", data-preview (log verdict, no side effects).
User manual + screenshots: huloglobal.com/vendure-plugins/geo-block/docs/
Commercial. Buy at elite.charity/licence/buy/vendure-plugin-geo-block.
All notable changes to @huloglobal/vendure-plugin-geo-block are documented
here. The format follows Keep a Changelog
and this project adheres to semantic versioning.
btn btn-secondary etc.) from the admin shell, but the
built admin CSS defines .btn only contextually and .btn-secondary
not at all — "+ Add" and friends rendered as bare native buttons.
Every control is now styled by the component itself (.gbtn system).--color-component-color-200/300, which the admin never defines, so
hardcoded slate fallbacks applied in both themes — unreadable on dark
surfaces. Replaced by per-theme ink tokens.#bfc3cc ≈ 1.7:1; dark border-200 is
darker than the surface, 1.01:1). Inputs/buttons/switch now use a
dedicated --gb-ui-border at ≥ 3:1 per theme.scripts/contrast-check.py
simulates the shipped color-mix() tokens against the real admin
theme values and checks every text/surface pair — 40/40 WCAG AA pass
(≥ 4.5:1 text, ≥ 3:1 UI boundaries) in light and dark.role="switch", keyboard + focus ring)
replaces the ON/OFF pill + button pair.role="tablist"/tab, aria-selected), labelled chip
remove buttons, aria-expanded on collapsibles, :focus-visible
rings on every control, prefers-reduced-motion support.color-mix()
instead of hardcoded light-mode pastels, which produced light-text-on-
cream unreadables in the dark theme (worst: selected preset cards).botAllowlist, defaulting
to 'strict' — matches every well-known SEO + social crawler
(Googlebot, Bingbot, DuckDuckBot, Baidu, Yandex, AppleBot, Slurp,
facebookexternalhit, Twitterbot, LinkedInBot, Slackbot, WhatsApp,
Discord, Telegram, Pinterest, Reddit + more). Ships crawler-safe
by default so restrictive geo rules don't silently drop search-engine
crawls and de-index the site. Also accepts 'permissive' (any UA
self-identifying as bot / crawler / spider), false (no allowlist —
use only when a WAF handles bots upstream), or a custom pattern
array of strings + regexes. New audit decision bot-allowlist.geoBlockSchedule — a JSON object with timezone, days, from,
to, and outsideAction (block / soft / allow). Recurring
weekly window per channel; outside the window the configured action
fires. Handles overnight windows and DST via Intl.DateTimeFormat.
New audit decision schedule. IP + bot allowlists still bypass so
ops and crawlers never see "closed for orders".GET /geo-block/hulo-geo.js.
One <script src> with data-channel-token="…" and the store gets
geo-blocking with zero custom code. Vanilla JS, no dependencies,
fails open on network error, ~2 KB minified. Optional attrs:
data-redirect, data-timeout-ms, data-preview. Cached
public, max-age=300, stale-while-revalidate=1200 (configurable via
new plugin option storefrontHelperMaxAgeSec).GET /geo-block/blocked?t=…&reason=….
Self-contained HTML — inline CSS, no deps, HULO amber-on-navy
identity. Renders channel blockLogoUrl, message, optional
redirect CTA, and support email (new plugin option supportEmail).
Returns JSON when Accept: application/json. Rate-limited.logo.svg in the package root
(globe + amber block-slash on the navy HULO frame)./geo-block/check now runs the bot allowlist and business-hours
schedule checks alongside the existing IP allowlist + maintenance
window checks. Precedence: IP allowlist → bot allowlist → schedule
→ maintenance → country / region rules.loadChannelRow now includes geoBlockSchedule and the previously-
missing geoBlockAllowedSubdivisions field, so subdivision rules
now round-trip correctly through the storefront /check path.geoBlockAllowedSubdivisions) was defined as a
channel custom field but never read into the runtime config on
/check — the storefront verdict ignored it. Now honoured
end-to-end (on licensed installs — unlicensed still forces the
map to empty per the 0.4 tier gates).warnIfIncompatibleVendure(). Logs a non-fatal warning when the runtime
@vendure/core version is outside the tested range. Silent when inside;
fail-open on unparseable versions.@vendure/core tightened to >=3.5.0 <4.0.0 — Vendure 3.5,
3.6 and 3.7 are all covered. Anything under 3.5 has never been tested;
anything from 4.0 upwards is deferred until the changelog is reviewed.@huloglobal/vendure-licence-sdk@^0.6.0.geoBlockPresets, geoBlockChannels, geoBlockStats
(paid), geoBlockSaveChannel, geoBlockSimulate (paid)./geo-block/check, /geo-block/site-config) stay
REST — they're anonymous, high-frequency, cacheable at the edge, and
GraphQL was never the right shape for them.Tier-gating on every premium feature via the SDK's isLicensed()
helper. Unlicensed installs get:
mode forced to block — no soft-block;Commenting out a boot check no longer unlocks anything — the gates are enforced at each call site.
Anti-tamper heartbeat via the SDK. Anonymous daily fingerprint of the embedded public key + verifier source. No personal data.
geoBlockAllowedSubdivisions storing a JSON map
{ "US": ["CA","NY"], "DE": ["BY"] }. Storefront enforcement checks
both the new map and the legacy GB-only field.GET /geo-block/subdivisions./site-config + /check.?country= override on /check (signingSecret).hashAuditIps, ipSalt).options.retention.@huloglobal/vendure-licence-sdk@^0.2.0.UpdateChecker integration — /geo-block/status endpoint returns
version + update info; admin banner appears on new releases.mode field (block or soft).
Soft mode renders the storefront with a "we don't ship here" banner
instead of hiding it.GeoBlockEvent entity records every block
decision (country, region, IP, UA, reason).GET /geo-block/admin/stats returns block totals,
top blocked countries, daily series and reason breakdown.POST /geo-block/admin/simulate dry-runs a
hypothetical visitor against current rules without persisting anything.blockMessage, blockRedirectUrl,
blockLogoUrl fields./geo-block/check endpoint — visitors can be checked
on the fly with logging to the audit table.GET /geo-block/presets lists every
preset with metadata (kind, description, country count)./geo-block/admin/* directly (no /ees/ prefix).isAllowed() and ipMatchesAny() exported for downstream use.GeoBlockPlugin registering five Channel customFields per channel
(enable toggle, region presets, allowed countries, blocked countries,
UK region sub-filter)./geo-block/site-config endpoint serving a flat resolved
allow-list per channel./geo-block/admin/channels and /geo-block/admin/save.resolveAllowedCountries exported as a pure helper.@huloglobal/vendure-licence-sdk with
revocation polling.