Meridian handbook
For agentsRecipes

Partner portal

How an agent sets up the member portal on engine-dmo for partner businesses, declaring membership tiers, the member_editable rules that say what each tier may change, an extra portal screen, an organisation with a membership and an invitation, and how to verify each part.

engine-dmo

This feature is part of the DMO bundle (engine-dmo).

The portal is where a partner business’s people sign in (with a link sent by email, or a passkey) to edit their listings, see their enquiries and their report, and manage their membership. It is served by the engine under /members on each site’s own host and works out of the box with the engine’s reference screens. Your work is to say who may change what, add any screens the brief needs, and invite the first partners.

Start from the DMO directory, whose listing has the fields named below. Examples use the locale code en; replace it with your source locale.

Declare the tiers

Tiers are ranked lowest first:

engine-dmo dev tier add standard --label Standard --apply
engine-dmo dev tier add silver --after standard --label Silver --apply
engine-dmo dev tier add gold --after silver --label Gold --apply
# config/memberships.yaml
tiers:
  - key: standard
    id: tier_qxgx1sr7ff
    label: { en: Standard }
  - { key: silver, id: tier_brwtq46ckb, label: { en: Silver } }
  - key: gold
    id: tier_q9nx12bh8p
    label: { en: Gold }

none is reserved for an organisation without an active membership; rules may name it.

Say what members may edit

Without a member_editable rule, members cannot edit documents of the type. Add one to the type’s schema file:

# config/schema/listing.yaml (top level, after fields:)
member_editable:
  tiers:
    none:
      fields: [phone, website, openingHours]
      approval: required
    standard:
      fields: [summary, description, phone, website, openingHours, heroImage]
      approval: required
    gold:
      fields: "*"
      except: [slug, area, category]
      approval: auto
      review_window: 24h
      always_review: [primaryTag]

approval: required sends every change to the admin’s moderation queue; auto publishes at once and staff review within review_window. Every field named here is on the directory’s listing, or is core. check reports an unknown field or tier, so name only your own type’s fields. engine-dmo describe type listing prints the rules.

Add a portal screen

The reference screens (dashboard, organisation, listing edit, photos, hours, enquiries, report, people, membership and more) need nothing from you. Add a screen of your own with a verb:

engine-dmo dev portal screen add offers --nav-order 30 --min-tier silver \
  --data 'listings=documents type=listing states=draft,published sort=-updatedAt limit=20' \
  --scaffold --apply
# config/sites/main/portal.yaml (written)
screens:
  - { key: offers, id: scr_yry1y0gjkw, path: "/organisations/{org}/offers", template: offers.html, nav: { group: business, order: 30 }, tiers: { min: silver }, data: { listings: { documents: { type: listing, states: [draft, published], sort: -updatedAt, limit: 20 } } } }

--scaffold writes templates/main/portal/offers.html, a complete page using the portal’s stylesheet with data.listings.items in a list. Edit it; give the page one h1. Forms posting to the portal are declared as actions (engine-dmo dev portal action add <subject.verb>); see Portal screens and actions. To restyle every portal screen, engine-dmo portal scaffold --site main copies the reference templates into the project. Always pass --site: without it the command writes to templates/portal/, which is on no site’s template chain, so nothing there renders and check warns that it is unused.

engine-dmo portal check
engine-dmo portal status

portal check checks your portal templates against the portal context; portal status lists every screen with its path and whether the engine or your project supplies its template.

Add a partner and invite them

In a seed, an organisation is a file under seed/organisations/ with its contacts, memberships and invitations, and a listing names it with a top-level organisation: <key>. Organisations and people has the file format, with an example. On a running database:

engine-dmo crm organisation create '{"name":"The Wharf Hotel","email":"stay@wharf.example","status":"active"}'
engine-dmo crm membership set <organisation-id> --tier gold --reason "Joined at the spring trade fair"
engine-dmo crm invite --organisation <organisation-id> --email owner@wharf.example --name "Sam Owner" --role "General manager"

organisation create prints the new id; membership set starts an active membership on the default site (--site for another); invite links the person as the organisation’s owner and queues the invitation mail. It prints no link. Linking an existing listing to an organisation is done in the admin’s document editor (the Organisation panel) or in the seed.

Verify

engine-dmo check
engine-dmo portal check
engine-dmo schema plan
engine-dmo describe type listing

The portal’s screens need a signed-in member, so render cannot show them. To see one you need the sign-in mail, and how you read it depends on your mail settings.

Read the mail

The log driver, the default, writes to, subject and tag at the info level, with no body and no link. Choose one of these in development:

# config/engine.yaml, to read the body in the server log
logging:
  format: ${LOG_FORMAT:-text}
  level: debug
# config/engine.yaml, to send to a local SMTP catcher such as Mailpit on port 1025
mail:
  driver: smtp
  from: hello@example.org
  smtp:
    host: localhost
    port: 1025
    tls: none

If you can do neither, say so and leave this step to the person.

Sign in

The invitation does not carry a token. Its link goes to /members/login, on the site’s first domain without a port, so in development it reads http://localhost/members/login and goes nowhere: open /members/login on your server’s own address. There the person asks for a sign-in link, and only that second mail has a token (/members/auth/<token>, on the address they asked from). It lasts 15 minutes.

A sign-in link works only in the browser that asked for it, which holds a __Host-mrd_link cookie. Opened anywhere else it answers 200 with Open this link in the browser where you requested it, and signs nobody in. So from a shell, keep one cookie jar across both requests:

curl -s -c jar -b jar -o /dev/null -X POST -d 'email=owner@wharf.example' http://localhost:8080/members/login
# read the /members/auth/<token> link from the mail, then:
curl -s -c jar -b jar -o /dev/null -w '%{http_code} %{redirect_url}\n' 'http://localhost:8080/members/auth/<token>'
curl -s -b jar http://localhost:8080/members

The second command answers 303 to /members. POST /members/login answers the same page whether or not the address is known, so an address that is not a person’s gets no mail and no error. See Partners and the portal.