intermediate
city-portal
magic-link
HMAC

City Portal & Magic-Link Auth

The /city/{slug} portal that city contacts log into via emailed magic link — bounded by the jurisdiction's geometry, read-only, with a 24h HMAC-signed session cookie.

Levy Fleets TeamMay 18, 202610 min read

City Portal & Magic-Link Auth

City compliance officers don't want another password. The Levy city portal at /city/{slug} uses email-based magic-link auth — enter your email, click a link, you're in. The session is HMAC-signed, lasts 24 hours, and is scoped to a single jurisdiction.

Who can log in

Only emails listed in city_contacts with portal_access = true can authenticate. Adding a city contact happens in the operator dashboard, not the portal itself — there is no self-service signup.

URL structure

PageURLPurpose
Login/city/{slug}Enter email, request magic link
Auth callback/api/city/{slug}/auth/callback?token=<tok>Consume the magic link, set session cookie, redirect to dashboard
Dashboard/city/{slug} (post-auth)Compliance scoreboard + links to other views
Fleet map/city/{slug}/fleetVehicles inside the jurisdiction's bbox
Trip heatmap/city/{slug}/tripsTime-bucketed trip density
Parking corrals/city/{slug}/parking-corralsUtilization snapshots
Compliance report/city/{slug}/compliance-report?period=monthly&date=2026-05Download CSV/PDF

The slug matches mds_jurisdictions.slug. One slug = one jurisdiction = one bbox.

1

Contact enters their email

On /city/{slug}, the form posts to POST /api/city/{slug}/auth/magic-link with { email }. The route always returns HTTP 200 — we don't reveal whether the email exists in city_contacts (avoids enumeration).

2

Token minted and emailed

If the email matches a city_contacts row with portal_access = true for this slug, the route generates a one-time token, stores its sha256 hash + expires_at = now() + 15 minutes on the contact row, and emails the link.

3

Contact clicks the link

The link goes to /api/city/{slug}/auth/callback?token=<tok>. The route hashes the token, looks up the matching city_contacts row, checks expires_at, marks the token consumed, and issues a session cookie.

4

Session cookie set

Set-Cookie: levy_city_session=<hmac>; Path=/city/<slug>; HttpOnly; Secure; SameSite=Lax; Max-Age=86400. The cookie payload encodes city_contact_id, jurisdiction_id, issued_at. HMAC-signed with a server-side secret.

5

Redirect to /city/{slug}

Now showing the post-auth dashboard. Every subsequent request to /api/city/{slug}/* verifies the HMAC and bbox-clips the response.

What the contact sees

The dashboard shows:

  • Compliance scoreboard — pass/fail per permit_conditions row, 30-day rolling
  • Today's snapshot — deployed fleet count, trips, open complaints
  • Active policies — the rules ingested from their own Policy feed (sanity check)
  • Links — to fleet map, trip heatmap, parking corrals, monthly report

Everything below the snapshot is read-only. The portal does not let the city edit operator data — they edit their own policy feed, and we re-ingest it.

Bbox clipping

Every city-portal API route is clipped to the jurisdiction's bbox before returning data. The clipping is enforced server-side, not at the UI layer:

// Inside every /api/city/{slug}/* route
const session = await verifyCitySession(request);
const jurisdiction = await getJurisdiction(session.jurisdiction_id);
const bbox = jurisdiction.bbox; // [minLng, minLat, maxLng, maxLat]

const vehicles = await sb
  .from('vehicles')
  .select('id, last_lat, last_lng, status, vehicle_number')
  .gte('last_lng', bbox[0])
  .lte('last_lng', bbox[2])
  .gte('last_lat', bbox[1])
  .lte('last_lat', bbox[3]);

A city contact will never see a vehicle outside their boundary, even if it exists in the same subaccount.

For the trip heatmap, clipping is performed on the trip's start point — a trip that began inside the jurisdiction and ended outside still appears on the heatmap. This is what cities expect for trip-origination analytics.

Session expiry + refresh

Sessions last 24 hours. There is no refresh flow — the contact requests a new magic link when the session lapses. This is deliberate: a city analyst losing access for a few hours and going through the magic-link motion again is acceptable; long-lived city portal sessions are not.

The HMAC secret can be rotated by setting a new value in environment config and redeploying. Sessions signed with the old secret immediately fail HMAC verification and the contact gets a clean re-login prompt.

Multiple contacts per jurisdiction

A jurisdiction can have any number of city_contacts rows. Common pattern:

RoleEmail
Lead compliance officersarah@city.gov
Permit reviewerpat@city.gov
Populus / Ride Report analyst (consultant)analyst@populus.ai

All three see the same data, all three receive digest emails on their own cadence. Sessions are independent — one contact logging out does not affect the others.

Revoking access

To revoke a contact's access:

  1. Open /dashboard/compliance/{jurisdiction-id} -> City contacts.
  2. Set portal_access = false on the row, or delete it outright.
  3. Active session cookies on that contact will fail server-side verification at the next request — the cookie is still valid HMAC, but the contact row no longer permits portal access.

Magic links already minted to that contact are also invalidated — auth/callback rejects tokens for contacts where portal_access = false.

Localization

The portal is rendered in the contact's language preference (city_contacts.locale, defaults to en). Digest emails also use the same preference. Launch supports en and es; additional locales fall back to English.

Troubleshooting

  • Contact says "didn't get the email" — check spam, then confirm the email matches city_contacts.email exactly (case-insensitive). Re-send by entering the email in the portal form again.
  • Link says "expired" — magic links live 15 minutes. The contact should request a new one.
  • Link says "invalid" — the token was already consumed, the contact row was deleted, or portal_access was set to false. Investigate in the operator dashboard.
  • Session cookie not setting — the contact is on a browser that blocks third-party cookies and /city/{slug} is being loaded inside an iframe somewhere. Avoid iframe embeds.

What's next

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.