S
supero.docs
Documentation/Reference apps/A real estate brokerage website in four schemas, open source as Haven

A real estate brokerage website in four schemas, open source as Haven

Haven is an open-source real estate brokerage website with listing search, tour requests and offers. Source on GitHub, live demo at haven.supero.live.

  • apps
  • real-estate
  • workflows
  • sagas
  • marketplace

Overview

Haven is an open-source real estate brokerage website: one brokerage's listings and offers in four schemas, where the broker's Accept button runs a workflow that knows how to reverse itself.
Accepting an offer on a house changes two records. The offer becomes accepted, and the listing it was made on stops being active. If the first write lands and the second does not, the brokerage holds an accepted offer on a home the public search still shows as available.
Haven handles that with a workflow called offer_accepted, declared in setup.py. Thirteen of the 19 reference apps in the repo define a workflow with on_error set to compensate. Haven's is the only one where two different record types each carry their own undo. The app's code is the haven folder of that repo.

What the Accept button runs

The workflow has three steps. The first sets the offer to accepted. The second emails the buyer. The third moves the listing to pending, and looks like this:
python
            {"id": "mark_listing_pending", "type": "crud_operation", "operation": "update", "object_type": "haven:listing",
             "record_uuid": "{{input.listing_uuid}}", "data": {"listing_state": "pending"},
             "compensate": {"type": "crud_operation", "operation": "update", "object_type": "haven:listing",
                            "record_uuid": "{{input.listing_uuid}}", "data": {"listing_state": "active"}}},
The compensate block is the reverse write: put the listing back to active. The first step has the same shape, with under_review as the value it restores on the offer. The email step in the middle is marked on_error: continue and has no compensate block, since a sent email has nothing to write back.
On the front end, the Offers tab of the broker console in ui/app.js calls the workflow through a small wrapper named runSaga. The wrapper rejects when the run reports any status other than completed, or any failed step. When it rejects, or when no workflow service is present, the console makes the two updates itself with plain update calls, so the Accept button in the demo completes either way. The broker's other buttons are single-field updates and skip the workflow entirely.

Four schemas, two of them readable without a login

schemas.py defines Listing, Agent, Tour and Offer, one schema per table, and nothing else. Listing and Agent are named in PUBLIC_SCHEMAS, so the search page works for a visitor who has never signed in. I fetched https://haven.supero.live/api/public/listing with no credentials on 2 October 2026 and got 14 listings back, the same 14 that setup.py seeds: 12 active, one pending, one sold. The agent endpoint returned eight.
The model is flat.
No schema references another, and a Tour or an Offer copies listing_title and agent_name as plain strings. That keeps every list on screen to one read, with no second fetch to resolve a name.
Search is done in the browser. The app loads the public listings once and filters them in memory with a text match on location and four dropdowns (listing type, property type, bedrooms, price band), and the search box on the home page hands its choices to the results page in the URL hash, as in #/search?city=Oakland&beds=2.

A buyer's portal that holds only that buyer's rows

Two roles carry policies. tenant_admin is the broker. tenant_user is the buyer, and the whole of the buyer's access is ten lines:
python
    PolicyDef(role="tenant_user", default_access="none", rules=[
        # Discovery — every signed-in buyer reads the public marketplace:
        PolicyRule(entity="listing", can_read=True),
        PolicyRule(entity="agent", can_read=True),
        # Private — each buyer owns only their own tours and offers:
        PolicyRule(entity="tour", can_read=True, can_create=True, can_update=True,
                   filter_field="owner_username", filter_match="$user.name"),
        PolicyRule(entity="offer", can_read=True, can_create=True, can_update=True,
                   filter_field="owner_username", filter_match="$user.name"),
    ]),
This is policy-based row scoping. The filter on owner_username is applied by the server, so the portal's "my tours" and "my offers" tabs are ordinary list calls with no where clause in app.js.
The broker's policy allows writes to all four entities. Delete is allowed on listings and agents only.
Requesting a tour does more than insert a row. An event binding on @create:haven:tour starts the tour_confirmation workflow, which has an email step and an SMS step, and the binding takes the email recipient from user.email, the address of the account that made the request. A second binding on @create:haven:offer starts offer_received, whose Slack step posts the amount and listing title to an #offers channel.
The demo buyer is seeded with five tours and five offers.

Where Haven stops and Lattice begins

Lattice is the repo's second real-estate app. The two share almost nothing. Haven runs one brokerage in one tenant; its config.py lists a single default-tenant, and the live config.js reports isMultiTenant: false. Lattice runs three property-management companies side by side, with eight schemas, units nested under buildings and a rent ledger.
Choose by date. Haven covers everything up to the moment an offer is accepted, and Lattice covers the years after someone moves in, when the questions are about leases, rent and repairs. Building for agents and buyers, start from Haven.

Clone the real estate brokerage website and press Accept

The full source is in apps/real-estate/haven. To run it:
bash
git clone https://github.com/supero-platform/supero-apps
cd supero-apps/apps/real-estate/haven
cp .env.example .env    # set SUPERO_DOMAIN and SUPERO_PASSWORD in it, then:
./run.sh
SUPERO_DOMAIN can be any free name. No account is needed, and it is running in about two minutes at http://localhost:5675. The app code is MIT. Supero, the hosted platform it calls, is not open source.
The login form at haven.supero.live lists two demo accounts: one buyer, one broker. On your own copy, sign in as the broker and open Offers. The offer on "Cozy Starter Condo" is seeded as submitted against an active listing. Accept it, then reload the public search and look at that card.
A different brokerage model is a different schema file, and that one you write at supero.dev.