Troubleshooting
The canonical checklist for when something in the compliance stack isn't behaving. Walks through MDS endpoints, the Policy ingester, real-time enforcement, the city portal, and digest emails — in that order, because that's roughly the dependency chain.
MDS endpoints
401 on every request
Symptoms: city's validator returns 401 Unauthorized on /vehicles/status.
Checks:
- The bearer token is missing from
Authorization: Bearer <token>. Cities sometimes paste the token without theBearerprefix. - The token was revoked. Open Settings -> API & Integrations -> MDS Tokens and confirm the token is active.
- The token belongs to a different subaccount. One token per subaccount; cities accidentally testing across multiple jurisdictions will see this.
Validator fails on version field
Symptoms: mds-provider-validator complains the response envelope reports a version other than 2.0.1.
Checks: confirm src/lib/mds/v2/response.ts has const MDS_VERSION = '2.0.1'. If you deployed a stale build, redeploy.
MDS-JWT header missing on a response
Symptoms: city expects every response to carry MDS-JWT: <jwt> and the header is absent.
Checks:
mds_jwks_keyshas nois_active = truerow for the subaccount. Hit/api/mds/<subaccountId>/.well-known/jwks.jsononce to force a lazy mint, then retry.- The keypair was generated but the active row has
private_key_pemcorrupted (rare — usually a manual edit gone wrong). Look at Sentry forMDS sign failedbreadcrumbs.
current_speed_limit_kph always 0
Symptoms: vehicles report current_speed_limit_kph: 0 even when they should be in a zone.
Checks:
- The vehicle has no last-known GPS position. Inspect
vehicles.last_lat/last_lngandvehicles.last_seen. - No operator zone or policy geofence contains the point. Run
SELECT * FROM zones_containing_point('<subaccountId>', <lat>, <lng>)and confirm at least one row is returned. - The active rule's
rule_typeisno_riderather thanspeed— that's a lock, not a speed limit. The status row should showis_disabled = trueinstead.
Policy ingester
City's Policy feed returns 404
Symptoms: audit log entries with status = 'failed' and errors containing HTTP 404.
Checks:
- The URL in
mds_jurisdictions.policy_feed_urlis wrong. Test from your terminal:curl -i <url>. Cities sometimes change paths between provisional and production endpoints. - The auth token expired. Try with and without the token to isolate.
Feed validates but yields no policy_geofences
Symptoms: audit shows status = 'success' and mds_policies rows exist, but policy_geofences is empty.
Checks:
- Every rule referenced geographies the city didn't publish. Inspect
errorsin the audit row — unresolved geography IDs are listed there. - The city published policies for a vehicle type your fleet doesn't operate. Confirm the rules'
vehicle_typesinclude at least one type you have vehicles for.
Same policy keeps appearing in every diff
Symptoms: every poll's diff shows the same policy in modified, even when nothing meaningful changed.
Checks:
- The city is regenerating the feed payload on every request (different
published_date, whitespace, ordering). The sha256 short-circuit fires only on byte-identical payloads. Ask the city to stabilize their serialization. - The city changed
published_dateautomatically — some CMSes do this. The diff is harmless but noisy; can be filtered out in the audit log UI.
Activation never fires
Symptoms: a policy is pending, start_date has passed, but the status never flips to active.
Checks:
- The
mds-policy-activatecron is failing. Check/api/cron/mds-policy-activatein Vercel cron logs. - The
start_dateis in the city's local time but stored as UTC without conversion. The cron compares againstnow() UTC; a mis-set timezone in the feed causes activation to fire much later than expected.
Real-time enforcement
Rule activated but no commands sent
Symptoms: mds_policies.status = 'active' but policy_enforcement_events has zero rows for the rule.
Checks:
- No vehicles are inside the geometry.
SELECT COUNT(*) FROM vehicles WHERE ST_Contains('<rule_geometry>', ...)— if zero, there's nothing to enforce. - All vehicles have stale GPS (>5 min). The skip is by design. Look for
error = 'stale_gps'events in the table. - The vehicles have no linked
iot_devicesrow. Skipped witherror = 'no_iot_device'.
Commands sent but vehicles don't slow down
Symptoms: policy_enforcement_events shows command_sent_at but no command_ack_at, and the vehicle continues at full speed.
Checks:
- OEM-specific OEM offline or asleep. Open the vehicle in the dashboard and inspect last_seen.
- The wrong command format reached the OEM. Inspect
command_responsefor an error code — most OEMs return rejection codes in plain text. - The vehicle's IoT password is wrong. Confirm
iot_devices.iot_passwordmatches what the OEM expects (see OKAI integration, Queclink integration, etc.).
Sub-fleet sees commands, another sub-fleet doesn't
Symptoms: OKAI vehicles obey the slow-zone, Segway vehicles don't (or vice versa).
Checks:
- The per-OEM dispatcher branch is missing or stale for the OEM. Confirm
src/lib/iot/dispatch.tshas a case for that OEM and the command format matches the OEM's protocol version. - Segway BLE-relay latency — the command was sent but hasn't been acked yet because the vehicle is far from any phone. This is a known gap; latency for Segway can exceed 30 seconds.
City portal
Magic link returns "invalid token"
Symptoms: contact clicks the emailed link and lands on an error page.
Checks:
- The token expired (15 minutes). Have them request a new one.
- The contact row was deleted or
portal_access = false. - Two links were minted in quick succession; the first consumed, the second appears stale.
Magic link email never arrives
Symptoms: contact submits the form, gets the "check your email" message, no email comes.
Checks:
- Spam folder.
- The email doesn't match a
city_contactsrow exactly (case-insensitive). Inspect the row. - The email infra rejected the send (Sentry will have the bounce). Look for spam-filter triggers on subject or body.
Portal shows no vehicles even though fleet is deployed
Symptoms: city contact logs in, the fleet map is empty.
Checks:
mds_jurisdictions.bboxis wrong — too tight or pointing elsewhere. Confirm against a map preview.- All vehicles have null
last_lat/last_lng. Inspect at least one.
Digest emails
Digest didn't arrive on the expected day
Symptoms: city contact expected a Monday weekly digest, didn't get one.
Checks:
last_digest_sent_atis in the current week — the cron decided the cadence window hadn't elapsed. Likely a previous digest fired in the same window.- The cron didn't run at all. Vercel cron logs for
/api/cron/compliance-digest. - The digest fired but the email send errored. Check
city_compliance_reports— if the row exists but no email landed, the failure is downstream.
Digest shows a failing condition that you've already fixed
Symptoms: scoreboard in the digest shows red on complaint_sla even though all complaints were responded to.
Checks:
- The digest covers a closed period (yesterday for daily, last week for weekly). It will not retroactively recompute. The current scoreboard at
/dashboard/compliance/{id}is the live view.
PDF attachment missing on monthly digest
Symptoms: monthly digest arrives but pdf_url is null and the attachment is absent.
Checks:
- The PDF renderer errored. Sentry will have the stack trace. The HTML/text body still fires; the PDF is best-effort.
Where to look in the database
| Question | Table |
|---|---|
| Why didn't my Policy poll succeed? | mds_policy_audit |
| What policies are currently active? | mds_policies WHERE status = 'active' |
| What rules came from each policy? | mds_policy_rules WHERE policy_id = ... |
| What geofences materialized from a rule? | policy_geofences WHERE rule_id = ... |
| What IoT commands fired for a rule? | policy_enforcement_events WHERE rule_id = ... |
| What digests has a contact received? | city_compliance_reports WHERE delivered_to_contact_id = ... |
| What jurisdictions does the operator have? | mds_jurisdictions WHERE subaccount_id = ... |
When all else fails
Email support@levyelectric.com with:
- Subaccount ID
- Jurisdiction slug
- The audit
run_idif the issue is in the ingester - The
policy_enforcement_events.idif the issue is in enforcement - The
city_compliance_reports.idif the issue is in a digest
We have direct access to logs and can usually pinpoint the failure within an hour.
Running a fleet on this hardware?
Levy Fleets runs scooters, e-bikes, mopeds and golf carts on the IoT hardware you already have, with integration pages for 29 vendors including Segway, Okai, Omni, Queclink, Invers and Comodule. Keyless unlock, geofencing, rider payments and rider support are included, and the Managed plan is a share of revenue with no per-vehicle software fee.