Appearance
Tenancy and Security
What this chapter covers
Korido carries many fleets on one system, and no fleet may ever see another's trucks. This chapter explains how that wall is built — an isolation model that lives in the query layer, not the database — plus who is allowed to reach what, how people prove who they are, how a support engineer safely stands in an owner's shoes, and how a customer gets a private window onto one shipment without an account.
The picture
Every request that touches fleet data passes through the same gate. A person proves who they are, that proof yields a verified tenant — the fleet they belong to — and from that point on every question the system asks the database carries that tenant's identity as a filter.
The single most important idea in this chapter: the tenant filter is the wall. The database applies no isolation of its own — the filter written into the query is the only thing standing between one tenant's rows and everyone else's. If a tenant-scoped query did not name its tenant, it would read everyone's rows. So Korido makes the tenant explicit in scoped query APIs, then proves the wall holds with a test suite that tries to cross it.
Boundary
There is no hidden database isolation layer rescuing a missing predicate. A tenant-scoped read is correct only when it carries the verified tenant filter or joins through a tenant-filtered parent.
How isolation actually works
Every tenant-scoped table carries a tenant_id column. Isolation is the discipline that every query against those tables includes the predicate tenant_id = <the caller's tenant>, and that the tenant on the right-hand side comes from the verified login token, never from anything the caller can choose.
Three things, working together, make this reliable rather than aspirational:
A verified tenant, carried in a typed scope. When a session is verified, the tenant id it proves is bundled with the database handle into a single scope value. Tenant query functions accept only that scope — never a bare database connection. The type system will not let you call those scoped query helpers without a scope, and the only way to build a scope is to supply a tenant id. "Forgot which tenant" therefore fails before the query helper can even be called.
The predicate is written into every query. The scope guarantees the tenant id is available; the author is responsible for putting it into the
WHERE. For an ordinary tenant table the predicate is a direct match ontenant_id. Tables that legitimately hold shared reference rows alongside tenant rows — waypoints, corridor anomalies, WhatsApp message logs — filter with "the row is shared (tenant_id IS NULL) or it is mine," so shared geography stays visible while private rows stay walled. Junction tables scope through their tenant-owned parent.A regression suite proves no read crosses the line. Because the wall is a convention that authors must uphold, it is verified empirically. A two-tenant isolation suite seeds two fleets into every table and asserts that a scope for fleet A never returns a single row belonging to fleet B. Crucially, these tests run as a database superuser — the most permissive possible condition — so a query that drops its predicate is caught by the suite failing, not by luck. A tenant query with no two-tenant test guarding it is considered unverified.
A small set of paths are cross-tenant by design and use a plain database handle with no tenant bound: telemetry ingestion (which must look a device up before it knows whose truck it is), the scheduled background jobs, the KORIDO admin portal, the first moment of a customer tracking lookup (before the link's owner is known), and the shared-intelligence jobs that learn corridor patterns across the whole fleet. These are explicit exceptions, reviewed as such. When one of them already knows the tenant, it still writes the filter — a service handle is permission to cross tenants deliberately, not license to skip the predicate.
The four roles and what each reaches
Korido has four roles. Two belong to a fleet; two belong to the platform.
| Role | Belongs to | Reaches |
|---|---|---|
| owner | a tenant | The full fleet workspace for their tenant — vehicles, drivers, trailers, missions, alerts, Route Guard, fuel, reports. Everything is filtered to their tenant. |
| driver | a tenant | Their own mission actions and receipt submissions through the mobile API, scoped to their tenant. Never another tenant's data. |
| admin | KORIDO | The admin portal: cross-tenant platform operations and support. admin is a platform-only role — never a way to be "an owner of every fleet." |
| viewer | a tenant | A read-only tenant role, recognized by the system for a future read-only access tier. |
The rule behind the table: a fleet role can only ever act inside its own tenant, and a platform role is never treated as a tenant member. A support engineer who needs to see a specific fleet's workspace gets there through impersonation, described below, which is audited.
Logging in: a phone and a one-time code
A person signs in with only a phone number and a six-digit code. A worker-configuration switch governs how the code travels during staged rollout: with delivery off, sign-in uses a controlled bypass path and no outbound message is sent; with delivery on, the same flow sends the six-digit code over WhatsApp.
The code lives for ten minutes and tolerates three wrong guesses before it is thrown away. Two per-phone limits bound abuse independently of any single code: at most five code requests an hour, and at most twenty verification attempts an hour — the verification cap is what actually makes brute-forcing a six-digit code hopeless. Phone numbers are masked wherever they would otherwise appear in logs.
Because the WhatsApp channel is brought live in stages, OTP delivery carries a worker-configuration switch for staged rollout. While the channel is still being provisioned, the switch lets a sign-in complete without dispatching an outbound message, so real authentication can be exercised end-to-end before proactive WhatsApp is enabled; flipping a single worker-config value then routes every code through live delivery once the channel is ready. Like every other credential, the switch lives in worker configuration rather than the repository, and it holds the pre-launch posture until delivery is deliberately turned on.
Sessions
A verified login yields a signed session token whose claims are read directly off the top of the token: who you are, your tenant, your role, and when the token was issued and expires. Nothing is nested or borrowed from an external identity provider — Korido issues and verifies its own tokens.
- The web apps (fleet app, admin portal) keep the session in
HttpOnlycookies scoped to the exact site that issued them. Server code verifies the cookie locally on each request; there is no round-trip to a central auth service. - The mobile and machine APIs carry the token as a bearer credential on each request.
Access tokens are deliberately short-lived — eight hours for a fleet user, four hours for a platform admin. A longer-lived refresh credential (a high-entropy random value, of which only a hash is ever stored, kept for thirty days) lets a session renew itself quietly in the background without asking the person to log in again. When a session expires while someone is mid-task, the app follows the expiry to the login screen rather than stranding them on a dead page.
Impersonation: support that stands in, on the record
When a KORIDO support engineer needs to see exactly what a fleet owner sees, they impersonate that owner. This is a real, verified session for that owner's tenant — not a special "god view" — so every isolation rule above applies unchanged, and support sees precisely the owner's data and nothing wider.
The guardrails are deliberate. Only owners can be impersonated — never drivers, never other admins. The handoff from admin portal to fleet app rides a single-use token that lives about two minutes. The resulting session is capped at twenty-four hours and cannot silently refresh, so support access always ends. And both the start and the explicit exit are written to the audit log, so "who looked at this fleet, and when" is always answerable. The session itself records that an admin is acting as the owner, keeping the real actor visible behind the stand-in.
Customer tracking links: signed, gated, expiring grants
A fleet owner can hand a customer a link to watch one shipment — no account, no login. That link is a narrow, revocable grant: a deliberate opening in the wall.
Four properties make the grant safe:
- Signed. Passing the phone gate issues a cookie stamped with a signature the server can verify. The signature folds in the link's identity, the caller's phone, a per-link secret nonce, and the link's expiry moment. A cookie cannot be forged, and it cannot be lifted from one link onto another.
- Gated. The customer must also enter a phone number that matches the shipment's contact. An anonymous caller who guesses or scrapes a link learns nothing: an invalid, revoked, or expired link returns the exact same "phone doesn't match" answer as a wrong phone, so the portal never confirms a link even exists.
- Expiring and revocable. Every link carries an expiry, and an owner can revoke one at any time. Because both the expiry and the nonce are baked into the cookie's signature, expiring a link — or rotating its nonce to revoke it — instantly invalidates every gate cookie already handed out, not just future visits.
- Narrow. What the customer sees is a purpose-built, allowlisted view — where the truck is, the estimated arrival, the delivery progress. It is assembled to exclude tenant identifiers, device identifiers, raw telemetry, driver phone numbers, and internal operational status. The gate and route lookups are also rate-limited per caller so the public surface cannot be used to probe the system or run its route engine for free.
Secrets and webhook authentication
Every signing key and provider credential — the session-token secret, the tracking-link signing secret, the telemetry-webhook secret, and the WhatsApp tokens — lives in Cloudflare's secret storage and is never committed to the repository; each Worker verifies against its own copy. Inbound webhooks are authenticated before they are trusted: the telemetry feed from the tracker provider must present its shared secret and originate from an allowlisted address range, and WhatsApp callbacks are checked against a verify token and a per-request signature. Rotating any of these has a real product effect — rotating the session secret ends active sessions on the Worker that verifies it, rotating the WhatsApp credentials can pause code delivery and notifications until the new value is live, and rotating the telemetry secret must be coordinated with the provider so live positions are never dropped — so rotation is treated as a deliberate, deploy-time operation.
Edge cases
- A query that forgets its tenant. It does not compile: tenant query functions cannot be called without a tenant-bearing scope. If one were somehow written by reaching for a raw handle, the two-tenant isolation suite — running as a superuser, the most permissive condition — fails when it returns a foreign row.
- A device with no fleet yet. Telemetry ingestion runs cross-tenant precisely because it must resolve a device to its owner before it knows the tenant. A registered device not yet linked to a vehicle still has its heartbeat recorded, but no tenant-scoped rows are written for it.
- Shared geography vs. private rows on the same table. Some tables hold both fleet-wide reference rows and per-tenant rows. Their queries admit shared rows (
tenant_id IS NULL) and the caller's rows, never another tenant's — so a shared corridor stays visible to all while private anomalies stay walled. - A stale support session. An impersonation session cannot refresh and dies within twenty-four hours, so support access is never open-ended. Every start and exit is on the audit record.
- A leaked or scraped tracking link. Without the matching phone it yields nothing, and it says nothing about whether the link is real, expired, or revoked. Revoking it — or letting it expire — voids the cookies already issued, because the signature is bound to the nonce and the expiry.
- A concurrent code-guessing burst. The per-code three-attempt counter can be raced by simultaneous guesses, so the real guarantee is the per-phone verification cap (twenty per hour), which holds regardless of concurrency.
What's ahead
Korido reserves a fourth role, viewer, for a read-only tenant tier: a member who can see the fleet workspace — the map, missions, alerts, reports — without the authority to change anything. The role is already recognized across the system, so turning the tier on becomes a matter of granting it rather than reworking the permission model. The next step is the read-only surfaces that pair with it, so an accountant, an insurer, or a client-side observer can be handed a genuine window into a fleet without a seat at its controls — every isolation rule in this chapter applying to them unchanged.
How it connects
- Data Architecture — the tables the tenant filter guards, and the read models each surface composes.
- Reliability — how the cross-tenant ingestion and background jobs stay correct under retries and failure.
- Part 7 — The surfaces — what each role actually sees in the fleet app, admin portal, driver app, owner app, and the customer tracking portal.