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) │
└────────────────────────────┘- Application. You apply for
location.readand/ornav.readthrough the developer portal. The application is reviewed against the platform's GDPR / ePrivacy posture: purpose, lawful basis, retention, deletion path. - 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.
- Runtime. The bridge enforces the manifest declaration on every
call (same gate every other family uses). A
permission_deniedenvelope 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:
| Question | What 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)
| Field | Type | Notes |
|---|---|---|
lat | number | WGS-84, [-90, 90]. Always present. |
lng | number | WGS-84, [-180, 180]. Always present. |
heading | number | null | Degrees CW from true north. null when stationary. |
speedMps | number | null | Ground speed, m/s. |
accuracyM | number | null | 1σ horizontal accuracy, m. |
at | string | ISO-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.readexposes 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.readgrant.
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.
Related
- Subscriptions guide — same lifecycle patterns as the read-only families.
- Best practices — data minimisation, storage-free designs, when not to ask for a scope.
MiniAppManifest— manifest reference.@i99dash/admin-sdk— different privileged tier (device-side commands, not data reads).
Privileged mini-apps
When to reach for `@i99dash/admin-sdk`, how the security model works, and how to write your first privileged call.
MCP server
Use AI coding agents (Claude, Cursor, Continue) against the i99dash docs over Model Context Protocol. Install in 30 seconds, structured search + per-page fetch.