Multi-tenant isolation fails when "which tenant is this?" gets answered in more than one place. Sooner or later one of those places trusts something the client sent.

So it is answered once. Properties, units, and anything else scoped to a landlord call this before touching the database:

import AppError from "../../../middlewares/appError.js";

// The single ABAC chokepoint shared by every module that scopes data to
// a landlord_admin's own tenant (properties, units, ...): a
// landlord_admin's landlord_id always comes from their own identity,
// never from request input. hqlead has no restriction (null = unrestricted).
export async function resolveLandlordIdForActor(actor, { landlordadminIdentityRepo }) {
  if (actor.type === "hqlead") return null;

  if (actor.type === "landlord_admin") {
    const identity = await landlordadminIdentityRepo.findById(actor.adminId);
    if (!identity) throw new AppError("Landlord admin not found", 404);
    return identity.landlord_id;
  }

  throw new AppError("Invalid actor", 403);
}

Two details carry the weight.

actor.adminId comes from a verified JWT, and the landlord id is read from the admin's own identity row — never from a query string, a body field, or a filter. A request that sets ?landlord_id=<someone else> cannot influence it, because the value is never read from there.

And HQLead returns null, meaning unrestricted. The platform operator's cross-tenant view and the landlord admin's scoped view are the same decision expressed once, rather than two code paths that can drift apart. The final throw means a new actor type added later is refused by default instead of silently inheriting someone's data.