i99dash docs
Develop

PII scopes (location, navigation)

How to apply for PII-tier permission scopes (location.read, nav.read) through the developer portal. Why there's no per-user runtime prompt.

Two scopes — location.read and nav.read — sit in the PII tier. Unlike the read-only families (media.read, climate.read, etc.), declaring them in manifest.permissions[] is not enough on its own. You also need an explicit approval per app id from the i99dash developer portal.

Want to read what's playing in the cabin or whether AC is on? That's a regular read-only family — declare it in the manifest, ship. Subscriptions guide.

This page covers the why, the what, and the how.

The two-step gate

┌───────────────────────────────┐    ┌────────────────────────────┐
│  1. Developer portal          │    │  2. Catalog at install     │
│     application + approval    │──▶ │     manifest validator     │
│     (one-time, per app id)    │    │     checks portal grant    │
└───────────────────────────────┘    └────────────────────────────┘


                                     ┌────────────────────────────┐
                                     │  3. Runtime bridge gate    │
                                     │     (every call)           │
                                     └────────────────────────────┘
  1. Application. You apply for location.read and/or nav.read through the developer portal. The application is reviewed against the platform's GDPR / ePrivacy posture: purpose, lawful basis, retention, deletion path.
  2. Catalog. A manifest that lists a PII scope without the matching approval on file gets rejected at publish / install. So the scope cannot reach an end-user device until step 1 cleared.
  3. Runtime. The bridge enforces the manifest declaration on every call (same gate every other family uses). A permission_denied envelope is returned for any handler that wasn't declared.

There is no per-user runtime prompt. End users never get a "this app wants your location, allow?" dialog at install time. The consent decision is captured upstream — at the developer-portal application — where it is reviewable, auditable, and revocable centrally. If a developer's approval is revoked, every install of that app id loses the scope on the next catalog refresh.

What's reviewed

When you apply for a PII scope, the platform reviewer expects:

QuestionWhat a good answer looks like
Purpose. Why does this app need it?"A nearby-charging-station widget needs lat/lng to filter the list. We don't store the position."
Lawful basis. Under GDPR Art. 6, why is processing lawful?"Legitimate interest under Art. 6(1)(f) — providing the requested feature; balanced by data minimisation (no storage) and the user's ability to uninstall the mini-app instantly."
Retention. How long do you keep it?"Not retained beyond the request. The lat/lng is read on each useLocation event, used to query our backend, and discarded."
Deletion. What happens to historical data when the user uninstalls or revokes?"We don't store historical data so nothing to delete. If we ever do, we will commit to a 24-hour purge from the same backend the request hit."

A vague application ("we use it for analytics") gets rejected. The review is not a rubber-stamp — see the best-practices guide for the broader data-minimisation stance the platform takes.

What you write in code

The SDK side is identical to any other family — that's the point of the per-family template. There is no special "consent" API call your app makes. Once portal approval clears + you list the scope in the manifest, client.location / client.navigation work like client.media.

// 1. Declare in manifest.json
{
  "id": "ev_charging_widget",
  "permissions": ["location.read", "callApi:/api/v1/charging"]
  // ...
}
// 2. Use the hook (or the imperative client)
import { useLocation } from '@i99dash/sdk-react';

export function ChargingWidget() {
  const { data: loc } = useLocation();
  if (!loc) return <Loading />;
  return <NearbyStations lat={loc.lat} lng={loc.lng} />;
}

If approval hasn't cleared yet (or the catalog hasn't seen the approval refresh), the bridge returns permission_denied envelopes on every call. The SDK forwards those to client.onPermissionDenied('location.read') so you can wire one analytics handler and route them to your error UI.

Wire shape (unchanged from the regular families)

FieldTypeNotes
latnumberWGS-84, [-90, 90]. Always present.
lngnumberWGS-84, [-180, 180]. Always present.
headingnumber | nullDegrees CW from true north. null when stationary.
speedMpsnumber | nullGround speed, m/s.
accuracyMnumber | null1σ horizontal accuracy, m.
atstringISO-8601 capture time.

Navigation is similar — active, destinationLabel, distanceRemainingM, etaSeconds, currentManeuver, distanceToTurnM, at. The destination label is the friendly name the user picked ("Home", "Work"); the host strips raw addresses where it can so analytics inside your mini-app don't leak full POIs by accident.

Strict zod schemas — extra fields fail validation, same as every other family.

What you cannot do (and why)

  • No raw address strings. nav.read exposes a friendly label only. If your app needs the full street, design around the user reading it on the head-unit map; the mini-app sandbox is not the surface for cross-referencing addresses with backend data.
  • No location history. The bridge is event-stream only — you see the current position when subscribed and any deltas thereafter. There is no "last 7 days" API.
  • No cross-app correlation. Each mini-app's WebView is a separate sandbox. You cannot read what another app is doing with its location.read grant.

Revoking an approval

A platform reviewer can revoke an approval at any time. The catalog backend re-checks approvals on each install / update; the next catalog refresh removes the scope from active installs. The bridge handler returns permission_denied from that point on, and your hooks fall back to whatever you passed for fallback.

You'll know an approval was revoked because the onPermissionDenied handler starts firing for a scope it didn't fire for before.

On this page