Skip to Content
Security and privacy

Security and privacy

<mapvx-website> runs inside your page, with your page’s origin and permissions. Besides rendering the directory, loading it has effects that your Content Security Policy (CSP), your privacy notice and your consent flow need to account for:

  • The visitor’s browser connects to the MapVX API and to map tile hosts.
  • As soon as main.js loads, the bundle loads Google Analytics 4 for MapVX’s own property, which then receives events from your page.
  • The bundled maps SDK reports its errors to MapVX.
  • The browser asks for location access (and, on iOS, motion access) only when the visitor uses a location feature.
  • The component stores one preference in localStorage.

This page describes the published version 1.6.1.

Content Security Policy

If your site sends a Content-Security-Policy header, it must allow what the component loads at runtime:

DirectiveWhat the component needs
script-srcmain.js, a module script, from your origin or a CDN, and the Google Analytics loader that the bundle appends to <head> (https://www.googletagmanager.com).
style-srcstyles.css, plus the <style> elements that the component injects into <head> at runtime. Allow those with a nonce or with 'unsafe-inline'.
style-src-attr'unsafe-inline': some Font Awesome icons, the current-location marker and the formatting of the Deals + Events text use inline style attributes, and nonces don’t apply to attributes.
img-srcdata: (icons inlined in styles.css), blob: (used by the map library in some browsers), and the hosts that serve place images.
connect-srcThe MapVX API, map tiles, sprites and label glyphs, error reports and Google Analytics hits.
worker-srcblob:: the map’s worker is embedded in main.js and started from a blob: URL.
font-srcNothing: the component loads no web fonts. Map labels are fetched through connect-src.

Without worker-src blob: (or child-src blob:), the map stays empty, and the only sign is a CSP violation in the browser console. There is no separate worker file that you could self-host instead.

The Deals + Events section of a place profile shows formatted text (HTML) from your MapVX content. Without 'unsafe-inline' in script-src, inline event handlers in that text, such as onclick attributes, don’t run.

The two policies below were tested with the published 1.6.1 bundle in Chromium, in map view with location tracking on and on a place profile with Deals + Events and a route from the current location, and produced no violations. For the general CSP reference, see MDN .

Allowlist policy

This policy is for a self-hosted bundle (main.js and styles.css served from your own origin, see Self-hosting), with a nonce for the injected styles. The example builds the header value in Node.js; the directives are the same for any server.

import { randomBytes } from 'node:crypto' // A new nonce for every response const nonce = randomBytes(16).toString('base64') const directives = { 'default-src': ["'self'"], 'script-src': ["'self'", 'https://www.googletagmanager.com'], 'style-src': ["'self'", `'nonce-${nonce}'`], 'style-src-attr': ["'unsafe-inline'"], 'img-src': ["'self'", 'data:', 'blob:', 'https:'], 'connect-src': [ "'self'", // MapVX API 'https://public-api.mapvx.com', 'https://api.mapvx.com', // Map tiles, sprites and label glyphs 'https://tiles.mapvx.com', 'https://indoorequals.mapvx.com', 'https://api.maptiler.com', 'https://lazarillo.app', // Error reports 'https://sentry.mapvx.app', // Google Analytics 'https://*.google-analytics.com', 'https://*.analytics.google.com', 'https://*.googletagmanager.com', // 3D model decoders (3D maps only) 'https://mapvx-glb-assets.s3.us-east-1.amazonaws.com', 'https://www.gstatic.com', ], 'worker-src': ["'self'", 'blob:'], // For your own fonts: the component loads none 'font-src': ["'self'", 'data:'], 'object-src': ["'none'"], 'base-uri': ["'self'"], } const csp = Object.entries(directives) .map(([name, sources]) => `${name} ${sources.join(' ')}`) .join('; ') // Send it with the HTML response: // res.setHeader('Content-Security-Policy', csp)

The HTML of the same response carries the same nonce (NONCE_VALUE below):

<head> <link rel="stylesheet" href="/mapvx-website/1.6.1/styles.css" /> <style nonce="NONCE_VALUE"> mapvx-website { display: block; width: 100%; } </style> </head> <body> <!-- Angular stamps this nonce on the <style> elements it injects --> <mapvx-website ngCspNonce="NONCE_VALUE" api-key="YOUR_API_KEY" institution-id="YOUR_INSTITUTION_ID" ></mapvx-website> <script type="module" src="/mapvx-website/1.6.1/main.js"></script> </body>
  • Loading from a CDN: add its origin, https://cdn.jsdelivr.net or https://unpkg.com, to both script-src and style-src, and pin the exact version you tested (see CDN).
  • Map hosts are data-driven: the map style that the API returns for each institution names the tile, sprite and glyph hosts. The list covers the hosts seen in MapVX map styles, plus indoorequals.mapvx.com, a MapVX indoor tile host that the tests didn’t reach. If the console reports a blocked map request, add that host.
  • Place images come from the hosts that your institution’s content uses; the placeholder shown for places without images is on images.ctfassets.net. That’s why img-src allows https:. Narrow it only if you know every image host.
  • 3D decoders: the last two connect-src hosts serve decoders for 3D models, which the SDK loads only for 3D maps. The component’s map is 2D, so they aren’t contacted in normal use; they were part of the tested policy.
  • Add your own site’s sources (scripts, fonts, analytics) to the same directives.

Nonce for injected styles

At runtime, Angular adds <style> elements with the component’s styles to <head>, and Font Awesome adds one more (<style id="fa-auto-css">). To allow them without 'unsafe-inline', pass Angular a nonce with the ngCspNonce attribute:

  1. Generate a new random nonce for every response and add it to style-src as 'nonce-…'.
  2. Put ngCspNonce with the same value on <mapvx-website>, or on any other element inside <body> (not on <body> itself), in the HTML that your server sends, so that it exists before main.js runs. Angular uses the first element that has the attribute.

If your framework creates <mapvx-website> in the browser, put the attribute on another element that is in the server HTML, for example <div ngCspNonce="NONCE_VALUE" hidden></div>.

The nonce covers every <style> element that Angular injects for the component, and Font Awesome’s fa-auto-css. It doesn’t cover:

  • The Google Analytics loader. The bundle adds that <script> without a nonce, and there is no option to pass one. Allow https://www.googletagmanager.com in script-src, or use 'strict-dynamic'.
  • Inline style attributes. They need style-src-attr 'unsafe-inline'. Browsers ignore 'unsafe-inline' in a directive that also contains a nonce, which is why attributes get their own directive.
  • styles.css. It’s a regular stylesheet, allowed by 'self' or by the CDN’s origin.

If you can’t generate a nonce per response, for example on fully static hosting, use style-src 'self' 'unsafe-inline' and leave out style-src-attr, which then falls back to style-src. With neither a nonce nor 'unsafe-inline', the browser blocks the injected <style> elements and the component renders without its layout styles.

Alternative with 'strict-dynamic'

For a nonce-based script policy, use a nonce and 'strict-dynamic' in script-src, and put the nonce on the main.js tag. Scripts that the bundle adds itself, including the Google Analytics loader, are then trusted without listing their hosts. The other directives stay as in the allowlist policy; this variant also produced no violations.

// Replaces script-src in the allowlist policy, before the header value is built directives['script-src'] = [`'nonce-${nonce}'`, "'strict-dynamic'"]
<script type="module" nonce="NONCE_VALUE" src="/mapvx-website/1.6.1/main.js"></script>
  • The same nonce value can serve script-src and style-src.
  • With 'strict-dynamic', browsers ignore host sources and 'self' in script-src. The nonce on the tag is what allows main.js, wherever it’s served from, and a host list can’t keep the Google Analytics loader out in this mode.
  • styles.css still needs its origin in style-src when it comes from a CDN.

Third-party services

These are the hosts that the visitor’s browser contacts because of the component:

HostPurposeWhen
public-api.mapvx.comMapVX API: places, configuration, institutions, search, place details and map styleOnce api-key is set
api.mapvx.comMapVX API: walking directionsWhen the visitor requests a route
tiles.mapvx.com, api.maptiler.com and other hosts named by the map styleMap tiles, sprites and label glyphsWhile a map is displayed
Your institution’s image hosts, images.ctfassets.netPlace images, and the placeholder for places without oneWhen cards and profiles are displayed
www.googletagmanager.com, *.google-analytics.com, *.analytics.google.comGoogle Analytics 4, MapVX’s own propertyAs soon as main.js loads, even without an api-key
sentry.mapvx.appError reports to MapVXOnly when an error is captured
www.google.comGoogle Maps, opened in a new tab by Driving directionsOnly when the visitor clicks that link

If you load the bundle from unpkg or jsDelivr, the browser also contacts that CDN.

What the MapVX API receives. Your API key (in the Authorization header), the visitor’s search terms, the IDs of the places they open and, when the visitor picks Use current location as the starting point of a route, their coordinates, which the API needs to calculate the route. Like any request from a browser, these requests also carry the visitor’s IP address and standard browser headers. The component makes no MapVX API request until api-key is set.

The API key is visible. It’s written in your page’s HTML as the api-key attribute, so anyone can read it.

Google Analytics

The bundle includes Google Analytics 4 and sends events to MapVX’s own GA4 property, on every page that loads main.js:

  • As soon as main.js is evaluated, before any <mapvx-website> exists and even without an api-key, it reuses window.dataLayer and window.gtag if your page defines them (or creates them), queues MapVX’s config command with the initial page view turned off, and appends the gtag.js loader from https://www.googletagmanager.com to <head>, without a nonce.
  • Each component event is sent with send_to set to MapVX’s measurement ID, so it goes only to MapVX’s property, not to yours.
  • There is no attribute or API to turn it off.

What the events contain. Every event carries these parameters:

ParameterContent
hostYour page’s host name (location.hostname).
sessionIdA random ID generated in the browser. It isn’t stored: a new one is created on each page load and when the element is removed.
apiKeyThe api-key value, which identifies your integration.
institutionIdThe selected institution (from institution-id, or the city the visitor picks).
parentPlaceIdThe building shown on the map, if any.

The events describe what the visitor does in the component: session start and end (with its duration), the screens they view (as internal paths such as /list?view=grid or /profile/<id>), the grid/map toggle, category filters, search terms and result counts, the IDs, names and aliases of the places they open or select (cards, markers, route origin and destination), the URLs of the place websites and driving-directions links they click, and clicks on the map and route controls. The visitor’s coordinates are never included: the location button logs only the click, and a route from the current location is logged as current-location.

On top of these parameters, gtag.js sends the data that Google Analytics collects by default with every hit, such as your page’s URL, and sets its first-party cookies (see Browser storage).

Activity on your page. MapVX’s config command applies to the whole page, not only to the component, so MapVX’s property also receives:

  • the events that your own code sends with gtag('event', …) without send_to;
  • the events that Google Analytics measures automatically under the settings of MapVX’s property. In a test in September 2026, these were scrolling, clicks on links to other sites, page views when your page changes its URL through the History API, and search terms read from your page’s query string (such as ?q=).

These automatic events depend on the settings of MapVX’s Google Analytics property, not on the bundle, so they can change without a new version of the component.

Consent. Account for this processing in your privacy notice and consent flow. The component has no switch of its own; the options are:

  • Load main.js only after the visitor consents. The Google Analytics setup runs when the bundle is evaluated, so the component can’t render before that point either.
  • Set Google Consent Mode defaults before main.js loads. MapVX’s tag uses your page’s dataLayer, so it follows them. In a test with analytics_storage set to 'denied', gtag.js set no cookies but still sent hits without them.
  • Block https://www.googletagmanager.com in script-src (allowlist policy). gtag.js doesn’t load, and the component keeps working. CSP ignores query strings when it matches sources, so you can’t allow your own gtag.js from that host and block MapVX’s: leaving the host out blocks both. Under 'strict-dynamic', a host list can’t block it.

If your page runs its own Google tags, check the result in your tag debugging tools.

Error reporting

In 1.6.1, error reports come from the bundled maps SDK: it sends its own errors, for example a failed request to the MapVX API, to MapVX’s error tracking at https://sentry.mapvx.app. Errors raised by the component outside the SDK are only written to the browser console.

  • The reporting client is created when the SDK initializes, once api-key is set, and it sends a report only when it captures an error.
  • It’s an isolated Sentry client: it doesn’t call Sentry.init(), and it leaves out the integrations that hook global state (global error handlers, timer and event-listener wrappers, automatic breadcrumbs for console, DOM, fetch, XHR and history, and sessions). It doesn’t touch window.onerror, window.onunhandledrejection or your own Sentry setup. One default integration patches Function.prototype.toString page-wide; the patch returns the original result for functions that aren’t Sentry’s.
  • A report carries the error message and stack trace, the full URL of your page (query string included), the User-Agent and Referer, the browser’s locale and time zone, the SDK release, and a trail of the SDK calls made before the error, with the IDs of some of the places involved.
  • There is no attribute to turn it off. Blocking https://sentry.mapvx.app in connect-src stops the reports; the browser then logs a CSP violation for each blocked report.

The scope of error reporting can change between versions: review What’s new before you upgrade.

Browser permissions

The component requests no permission on load. On render it only reads the geolocation permission state, which doesn’t show a prompt.

PermissionWhen the browser asksWhat it’s used for
GeolocationWhen the visitor clicks the map’s location button, or picks Use current location as the starting point of a route.Showing and following the visitor’s position; a route’s starting point.
Motion and orientationOn browsers that require explicit permission (iOS Safari), when location tracking starts.Rotating the location marker to the compass heading.
  • If the visitor denies location access, the location button does nothing and Use current location is hidden. The rest of the component keeps working.
  • Once tracking is on, the position is watched continuously, with high accuracy, while a map is displayed. Tracking pauses when the map leaves the screen and resumes when a map is shown again.
  • Positions stay in the browser, except for a route’s starting point, which is sent to the MapVX API as described in Third-party services.
  • Geolocation requires a secure context (HTTPS). If your page is embedded in a cross-origin <iframe>, the iframe needs allow="geolocation"; a same-origin iframe has access by default. If your site sends a Permissions-Policy header, it must not disable geolocation.
  • The component asks for no other permission (camera, microphone, notifications…).

Browser storage

StorageKeyContentWritten by
localStoragemapvx:active-viewgrid or map: the visitor’s last Grid View / Map View choice.The component, when the visitor clicks the toggle.
Cookies_ga and relatedGoogle Analytics identifiers.gtag.js, as in any Google Analytics installation.
  • mapvx:active-view applies to the whole origin: every page and every <mapvx-website> on your site shares it, and it overrides default-to-map on later visits (see Remembered view).
  • If storage is unavailable, for example because the browser blocks site data, the component ignores the error and falls back to default-to-map.
  • The component sets no cookies itself. Neither it nor the bundled SDK stores data in sessionStorage, IndexedDB or the Cache API, and neither registers a service worker; the SDK’s caches live in memory only.
  • The analytics session ID is not stored.

If a policy or permission issue shows up as a visible symptom, see Troubleshooting.

Last updated on