Meridian handbook
Destination platform (engine-dmo)

Partners and the portal

Membership tiers and memberships, what each tier may edit on a listing, partner roles and their permissions, the member portal's sign-in, invitations, reference screens and configuration, and the enquiry inbox businesses work from.

engine-dmo

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

A partner is a business or body the destination works with, held as a CRM organisation. It may hold a membership on a site at a tier; its people sign in to the member portal to keep their listings, photos and opening hours current, read their report, answer enquiries and ask for promotions. What a person may do there depends on two things: their partner role in the organisation, and the organisation’s tier on the site.

This page covers tiers, memberships, editing rules, roles, the portal itself and enquiries. Adding your own portal screens and forms, restyling the portal and the member API are in Portal screens and actions.

Tiers

The install’s tiers are declared once, in rank order, lowest first:

config/memberships.yaml
tiers:
  - key: bronze
    label: { en-GB: Bronze }
  - key: silver
    label: { en-GB: Silver }
  - key: gold
    label: { en-GB: Gold }
KeyMeaning
keyLowercase letters, digits and hyphens. Every rule that names a tier is checked against this list.
idThe stable id (tier_), written by check --fix. Memberships are stored against it, so renaming a tier keeps them.
labelThe tier’s name per locale

engine-dmo dev tier add <key> [--after <tier>] [--label <name>] declares one with its id, and engine-dmo dev tier rename <old> <new> renames a tier everywhere it is named, keeping its id and memberships.

none is the tier of an organisation with no active membership on the site. It is reserved: you cannot declare it, but rules that apply to organisations (member_editable, slots, screens, actions) may name it.

A site sells every tier unless its site.yaml lists the ones it sells:

config/sites/town/site.yaml
memberships:
  tiers: [silver, gold]

Rules that name a tier a site does not sell are warned about at that site, since no organisation there can hold it.

Tier lists in rules

Wherever a rule takes tiers: (portal screens and actions, promotion slots, report digests, the booking link prompt) it is either a list of keys or the lowest tier by rank:

tiers: [silver, gold]   # exactly these
tiers: { min: silver }  # silver and every tier ranked above it

Omitted, a rule applies to every tier and to none. A list that names tiers excludes none unless it names it. { min: ... } needs config/memberships.yaml.

Memberships

A membership is one organisation’s on one site. It has:

PropertyMeaning
TierOne of the install’s tiers that the site sells
Statuspending, active, lapsed or cancelled
Joined on, renews on, ended onDates. Lapsing or cancelling sets the end date.
Amount, currency, external reference, notesWhat was charged and the invoice or order reference; for the record only
HistoryEvery change, with its reason

Only an active membership gives its tier. A pending one does not; an organisation with none, or only lapsed or cancelled ones, is at tier none.

Staff manage memberships on the organisation’s page and on the site’s Memberships screen (under Members), which counts active, pending and lapsed members and lists renewals due. A membership’s status changes only when someone changes it (or a paid order does); passing the renewal date does not lapse it.

engine-dmo crm membership set <organisation-id> --tier gold --site main --reason "Upgraded at the 2026 renewal"

crm membership set changes the organisation’s pending or active membership on the site, or starts one (status active unless --status says otherwise).

Renewal reminders

Each active membership whose renewal date comes within one of the configured intervals is sent the renewal-notice mail, once per interval per renewal date, in the site’s timezone. It goes to the organisation’s contacts with the billing role, else its primary contacts, else every current contact. A suppressed address is recorded on the organisation’s timeline instead.

config/engine.yaml
membership:
  renewal:
    reminders: [60d, 30d, 7d, 0d]
KeyDefaultMeaning
membership.renewal.reminders[60d, 30d, 7d, 0d]Days before renews_on, each a whole number of days from 0d to 366d. An empty list sends none.

The reminder carries a link to the site’s portal when the portal serves screens.

What members may edit

A type’s member_editable rule says, per tier, which fields of a document the organisation’s people may change from the portal and the member API, and whether their changes wait for staff. Without a rule, members cannot edit documents of the type.

config/schema/listing.yaml
extends: listing
fields:
  # ...
member_editable:
  tiers:
    none:
      fields: [phone, website, openingHours]
      approval: required
    silver:
      fields: [summary, description, phone, website, openingHours, heroImage]
      approval: required
    gold:
      fields: "*"
      except: [slug, area, category]
      approval: auto
      review_window: 24h
      always_review: [primaryTag]
      links: { follow: false, max_per_field: 5 }
KeyMeaning
tiers.<tier>A tier key, or none
fieldsThe fields the tier may change, or "*" for every field on the site
exceptFields taken out of "*"
approvalrequired: every change waits for staff. auto: changes publish at once and staff review them afterwards.
review_windowWith auto, how long staff have to review a change after it went live, such as 24h
always_reviewWith auto only, fields whose changes still wait for staff
links.followtrue leaves links members write without rel="nofollow ugc". Default false.
links.max_per_fieldThe most links in one rich text field. Default 3; a negative number removes the cap.

A tier the rule does not name may edit nothing. Fields are named by handle; check reports an unknown field, a field that is not on a site the rule applies to, a tier config/memberships.yaml does not declare, and always_review on a tier without approval: auto. A type’s per-site schema file (config/sites/<site>/schema/<type>.yaml) may set its own member_editable, which replaces the shared one on that site.

A member’s changes are revisions of the document. Those that wait appear in the admin’s moderation queue for staff to approve or return, with the expected turnaround from approvals.expected_turnaround in engine.yaml; see Documents, revisions and publishing.

engine-dmo describe type listing prints a type’s member rules with its fields, and engine-dmo api member form --listing <id> --person <id> prints the form a given member would receive.

Partner roles

A person’s partner role in an organisation decides what they may do for it. Roles are install-wide. Four are built in:

RoleGrants
ownerEverything (*), always. Its grants cannot be changed. An organisation always keeps at least one owner.
managerlisting.*, photos.*, hours.*, organisation.update, event.submit, report.view, enquiries.view, enquiries.manage, promotion.request, screen.*
contactreport.view, enquiries.view
billingmembership.view, report.view, billing.pay

The permissions:

PermissionAllows
profile.updateChanging one’s own details. Everyone signed in holds it; it cannot be taken away.
organisation.updateChanging the organisation’s contact details
listing.updateEditing the organisation’s documents within the tier’s member_editable rule
photos.add, photos.removeAdding and removing photos on a listing
hours.updateChanging opening hours
event.submitSubmitting an event
people.inviteInviting a colleague, in a role holding no permission the inviter lacks
people.manageChanging colleagues’ roles and removing them
promotion.requestAsking for a promotion
billing.payPaying for a tier or a promotion (paid listings)
enquiries.view, enquiries.manageReading the enquiry inbox, and changing an enquiry’s status, note and fields
report.viewThe member report
membership.viewThe organisation’s memberships
pending.viewChanges waiting for staff
action.<key>, screen.<key>A portal action or screen a site declares (see Portal screens and actions)

A grant may end in .* to cover a subject (listing.*, screen.*, action.*), and * covers everything.

Add roles, or change the grants of manager, contact and billing, in config/portal/roles.yaml:

config/portal/roles.yaml
roles:
  - key: marketing
    label: { en-GB: Marketing }
    can: [listing.update, photos.*, report.view, enquiries.view]
  - key: contact
    can: [report.view, enquiries.view, enquiries.manage]
KeyMeaning
keyThe role’s handle
idThe stable id (role_), written by check --fix for a role of your own. A built-in role keeps the engine’s id; do not write one for it.
labelThe role’s name per locale
canPermissions and patterns. check refuses an unknown permission, and an action.<key> or screen.<key> no site declares. Owner takes no can.

engine-dmo dev role add <key> --can <permissions> [--label <name>] declares or restates a role. Without the file, the four built-in roles apply as shown.

Every permission is checked in the database against the person’s current link to the organisation and the site’s network, never in a template. A person with no current link to an organisation can do nothing for it.

The portal

The portal is served by the engine on each site’s own host, under the portal path. It renders the engine’s reference screens in a neutral style until you restyle it, and it works without JavaScript.

Turning it on and off

config/portal/portal.yaml
portal:
  mode: both
  templated:
    enabled: true
    template_dir: portal
    routes:
      organisation: "/businesses/{org}"
  invitations:
    expiry: 30d
    reminder_after: 7d
KeyDefaultMeaning
portal.modebothtemplated: the engine renders the portal’s screens. api: only the member API answers, for a portal you build elsewhere. both: screens and API. off: nothing is served under the portal path.
portal.templated.enabledtruefalse stops the engine rendering screens in every mode
portal.templated.template_dirportalThe directory, under each template root, that holds your portal templates
portal.templated.routes.<screen>see Reference screensA reference screen’s path under the portal path
portal.invitations.expiry30dHow long an invitation link works
portal.invitations.reminder_after7dWhen an unaccepted invitation is sent a reminder
portal.report.*see Member reportsThe member report’s comparison, improvement prompts and digest

The file is install-wide. A site can change its own mode, path, template directory, routes and hidden screens with config/sites/<site>/portal.yaml (see Per-site portals); anything it leaves out keeps the install’s value. engine-dmo config schema --file portal prints the file’s schema.

The portal path is server.portal_path in engine.yaml, /members by default, and the member API is server.api.member.enabled (default true).

Signing in

Members sign in with a link sent to their email address, and may add a passkey from their account page. There are no member passwords.

Key in engine.yamlDefaultMeaning
auth.member.enabledtrueThe member realm
auth.member.magic_link.enabledtrueSign-in links by email
auth.member.magic_link.ttl15mHow long a sign-in link works
auth.member.passkeys.enabledtruePasskeys
auth.member.password.enabledfalseMust stay false: member passwords are not built
auth.member.session.absolute_lifetime720hThe longest a member session lasts
auth.member.session.idle_timeout336hHow long an unused session lasts
auth.member.registrationclaimclaim: staff invitations and the public claim form. invite_only: invitations alone; the claim form answers not found.

A member session belongs to the site’s host and to its network: a session on one site is not signed in on a site of another network.

Invitations

A person becomes a portal user by accepting an invitation; a seed file can invite one too (Organisations and people). Staff invite from the organisation’s page in the admin or with:

engine-dmo crm invite --organisation <id> --email jo@example.org --name "Jo Bloggs" --role "General manager" --site main

The command links the person as the organisation’s owner and queues the invitation mail; it prints no link. The mail links to the portal’s sign-in page, /members/login on the site’s first domain, and carries no token. There the person asks for a sign-in link, which is only valid in the browser that asked for it (a __Host-mrd_link cookie), so opening it elsewhere asks them to go back. To read these mails on a development install, see Reading mail in development. Members with people.invite invite colleagues from the portal’s people screen, in any role holding no permission they lack. An approved claim sends an invitation too.

Reference screens

Screen keyDefault path under the portal pathShows
dashboard/The organisations the person acts for
organisation/organisations/{org}An organisation: its listings, links to the other screens, the site’s actions, a first-session checklist
report/organisations/{org}/reportThe member report; needs report.view
contact_details/organisations/{org}/detailsThe organisation’s contact details; organisation.update to change them
people/organisations/{org}/peopleColleagues, roles and invitations
membership/organisations/{org}/membershipMemberships by site; needs membership.view
contact_team/organisations/{org}/contactA message to the destination’s team, recorded on the organisation’s timeline
enquiries/organisations/{org}/enquiriesThe enquiry inbox; needs enquiries.view
events/organisations/{org}/eventsThe organisation’s events, and submitting one; needs event.submit
promotions/organisations/{org}/promotionsPromotion requests; needs promotion.request
listing_edit/listings/{id}Editing a document within the tier’s rule; needs listing.update
photos/listings/{id}/photosA listing’s photos
hours/listings/{id}/hoursA listing’s opening hours

A route override must start with /, keep exactly one placeholder of the screen’s kind ({org} or {id}) as a whole path segment, and not take another screen’s path. The first path segments login, auth, logout, account, passkeys, claim, digest, invitations and _assets are the engine’s own and cannot be used.

Beside these, the portal serves sign-in, the account page (details and passkeys), invitation acceptance, the digest opt-out, the claim flow under /claim/, the billing screen at /billing/{org}, and any screens a site declares.

engine-dmo portal check prints each site’s mode, path, template directory and every screen’s route, and checks your portal templates against the portal context.

Enquiries

The enquiry inbox lists the enquiries an endpoint’s store step saved about one of the organisation’s documents and shared with it (store: { as: enquiry, retention: 90d, subject: $listing, share_with_organisation: true }, see Directory), in full, newest first, with the listing each was about. A person whose role grants enquiries.view reads them; enquiries.manage changes them.

Each enquiry has a status, a private note and the install’s enquiry fields.

StatusMeaning
newNot yet opened
readOpened
repliedMarked as answered
archivedOut of the inbox

The inbox’s filters are open (everything not archived, the default), each status, and all.

A person with enquiries.manage moves an enquiry between the statuses like this:

stateDiagram-v2
  [*] --> new : the post is stored and shared
  new --> read : opened, or a note added
  read --> new : mark unread
  replied --> new : mark unread
  new --> replied : mark replied
  read --> replied : mark replied
  new --> archived : archive
  read --> archived : archive
  replied --> archived : archive
  archived --> read : unarchive (never replied)
  archived --> replied : unarchive (replied before)
  new --> [*] : retention passes
  read --> [*] : retention passes
  replied --> [*] : retention passes
  archived --> [*] : retention passes

Marking an enquiry replied, or writing a note or a field on it, extends its retention once to the store step’s while_open period when it has one.

Declare the fields businesses keep against an enquiry in config/portal/enquiries.yaml, with the same grammar and the same eight types as CRM custom fields (text, number, boolean, select, date, datetime, phone, money). The names intent, note, status and return are reserved.

config/portal/enquiries.yaml
fields:
  - { name: followUpOn, type: date, label: { en-GB: Follow up on } }
  - name: stage
    type: select
    label: { en-GB: Stage }
    options:
      - { value: quoted, label: { en-GB: Quoted } }
      - { value: booked, label: { en-GB: Booked } }

An enquiry is kept for the retention its store step set and then deleted.

Configuration at a glance

FileHolds
config/memberships.yamlThe tiers
config/sites/<site>/site.yaml memberships.tiersThe tiers a site sells
member_editable in config/schema/<type>.yamlWhat each tier may edit
config/portal/roles.yamlPartner roles
config/portal/portal.yamlThe portal’s mode, templates, routes, invitations and report settings
config/sites/<site>/portal.yamlA site’s own portal, screens and actions
config/portal/enquiries.yamlEnquiry fields
engine.yaml membership:, auth.member:, server.portal_pathRenewal reminders, member sign-in, the portal path

Every one is checked by engine-dmo check with the problem’s file, line and column. After a change that adds ids or tiers, run engine-dmo check --fix, then engine-dmo schema plan and schema apply.