S
supero.docs
Documentation/Reference apps/Why Lattice, a property management app, calls its renters Residents

Why Lattice, a property management app, calls its renters Residents

Lattice is open-source multi-tenant property management software with leases, rent and maintenance requests. On GitHub, live at lattice.supero.live.

  • apps
  • real-estate
  • multi-tenant
  • property-management
  • access-control

Overview

Multi-tenant property management software with its source open: three management companies on one deployment, each with its own buildings and rent ledger, and one field on every rental application that is removed from the applicant's copy.
Property management has a vocabulary problem on a multi-tenant platform. In the industry a tenant is the person who pays rent. On Supero a tenant is an organisation whose data is kept apart from every other organisation's. Lattice needs both ideas at once, and its schemas.py settles it in a comment above the renter's schema: NEVER named "Tenant" (that's the Supero multi-tenancy concept) or "user".
So the renter is a Resident.
The tenant is the company that manages the building.

Summit, Harbor and Oakline each get a tenant

config.py, in the repo's lattice folder, declares four tenants. One is default-tenant, displayed as Lattice HQ and holding no business data. The other three are management companies: Summit Residential, Harbor Properties and Oakline Management. The seed code in setup.py keeps one plan per company and writes each into its own tenant:
python
COMPANIES = {
    "summit-residential": SUMMIT,
    "harbor-properties": HARBOR,
    "oakline-management": OAKLINE,
}


def seed_test_data(s, base, domain, tenant_uuid, progress):
    # default-tenant stays admin-only (no business data). Each PM company seeds
    # into its OWN named tenant.
    for tenant, plan in COMPANIES.items():
        _seed_company(s, base, domain, tenant, progress, plan)
    progress.ok("Seeded %d property-management companies." % len(COMPANIES))
I counted all three seed plans. Between them they hold 7 properties, 17 units, 4 owners, 4 residents, 4 leases, 6 rent payments, 5 maintenance requests and 4 applications. Summit has the most: three buildings in Austin and Round Rock. Oakline has two in Columbus. The hosted demo is shared and has picked up extra rows since it was seeded: on 2 October 2026 its public endpoint returned 11 properties.
There are six accounts: an HQ admin, a manager for each company, and two residents. A Summit manager who signs in gets Summit's console and nothing from Harbor. The repo README puts the guarantee this way: the isolation is enforced by the server, and a user signed into one tenant cannot read another tenant's rows even by calling the API directly with their own valid token.
The HQ admin is the exception by design. ui/app.js has a TenantSwitcher component, a dropdown labelled "Acting as company", which calls client.setTenantOverride and reloads the console under the chosen company. It renders only when client.canSwitchTenant() returns true. The hosted demo's config.js reported isMultiTenant: false on 2 October 2026, so the dropdown may not appear there, and .env.example turns the flag on for your own copy.

A Unit lives inside its Property

Lattice has eight schemas, one per table: Property, Unit, Owner, Resident, Lease, RentPayment, MaintenanceRequest and Application. Seven hang directly off the tenant. Unit does not. Its parent_type is "property", which makes every unit a child record of one building. When I fetched /api/public/unit from the live app on 2 October 2026, every row came back with parent_type set to property and a parent_uuid.
Property and Unit are the two public schemas, so the logged-out landing page can list available units. Everything else needs a login.
The comment block at the top of schemas.py records why each schema is shaped the way it is, and the most useful entry is about when to build on a platform service and when to leave it alone: RentPayment extends payment:base_payment and inherits a four-state lifecycle that runs from pending to refunded along with its mandatory amount and currency fields, while Lease stays a plain entity because the platform's rental base models short-term check-out and check-in, and MaintenanceRequest stays plain because the ticket base uses different state names from the submitted and assigned wording a property manager expects.
Rent moves through that inherited state machine. A resident's Pay rent screen calls client.transactional.payment.authorize and then capture on the pending charge. Maintenance is a four-column board, and the last move, into Resolved, runs a maintenance_resolved workflow that emails the resident and stamps processed_at on the request.

The screening notes an applicant does not get

A prospective renter can read their own application. The leasing team writes notes on that same record. One policy rule keeps the two apart:
python
        PolicyRule(entity="application", can_read=True, can_create=True, can_update=True,
                   filter_field="owner_username", filter_match="$user.name",
                   hidden_fields=["notes"]),
For a tenant_user, notes is absent from the response body, removed server-side. Managers hold tenant_admin and get the field.
That rule is the third layer of scoping in the app. The tenant decides which company's data a request can touch. The filter_field narrows a resident to rows where owner_username matches their login, which is policy-based row scoping and covers five schemas, their lease and their rent payments among them. hidden_fields then removes a column from a row the caller is otherwise allowed to read. The Owner schema has no tenant_user rule at all, and with default_access="none" that means residents cannot read building owners.

Haven finds the buyer; Lattice collects the rent

The other real-estate app in the repo is Haven, a listings marketplace for a single brokerage. Haven has four flat schemas in a single tenant and takes no payments. It ends when an offer is accepted. Lattice begins after the lease is signed, and its model is correspondingly heavier: nested records, six schemas with real references, a payment state machine, and three organisations that must not see each other.
Pick Lattice as a starting point if your customers are the companies, and each of them has customers of its own.

Run the property management app, then sign in as Dana

The source is in apps/real-estate/lattice.
bash
git clone https://github.com/supero-platform/supero-apps
cd supero-apps/apps/real-estate/lattice
cp .env.example .env    # set SUPERO_DOMAIN and SUPERO_PASSWORD in it, then:
./run.sh
You do not need an account, and Lattice is running in about two minutes on port 5713. The app source is MIT licensed. It runs against Supero's hosted platform, whose own code is not open source.
The login page at lattice.supero.live lists the demo accounts. Dana Reyes is a Summit resident in Apt 2B at The Monarch. Her seed data includes a pending rent charge of $2,150 for the period 2026-06 and a maintenance request titled "Leaking kitchen faucet" in submitted. Pay the rent as Dana, then sign in as the Summit manager and find the same payment in the rent ledger.
For a model with your own companies and residents in it, the starting point is supero.dev.