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
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:
tiers:
- key: bronze
label: { en-GB: Bronze }
- key: silver
label: { en-GB: Silver }
- key: gold
label: { en-GB: Gold } | Key | Meaning |
|---|---|
key | Lowercase letters, digits and hyphens. Every rule that names a tier is checked against this list. |
id | The stable id (tier_), written by check --fix. Memberships are stored against it, so renaming a tier keeps them. |
label | The 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:
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:
| Property | Meaning |
|---|---|
| Tier | One of the install’s tiers that the site sells |
| Status | pending, active, lapsed or cancelled |
| Joined on, renews on, ended on | Dates. Lapsing or cancelling sets the end date. |
| Amount, currency, external reference, notes | What was charged and the invoice or order reference; for the record only |
| History | Every 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.
membership:
renewal:
reminders: [60d, 30d, 7d, 0d] | Key | Default | Meaning |
|---|---|---|
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.
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 } | Key | Meaning |
|---|---|
tiers.<tier> | A tier key, or none |
fields | The fields the tier may change, or "*" for every field on the site |
except | Fields taken out of "*" |
approval | required: every change waits for staff. auto: changes publish at once and staff review them afterwards. |
review_window | With auto, how long staff have to review a change after it went live, such as 24h |
always_review | With auto only, fields whose changes still wait for staff |
links.follow | true leaves links members write without rel="nofollow ugc". Default false. |
links.max_per_field | The 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:
| Role | Grants |
|---|---|
owner | Everything (*), always. Its grants cannot be changed. An organisation always keeps at least one owner. |
manager | listing.*, photos.*, hours.*, organisation.update, event.submit, report.view, enquiries.view, enquiries.manage, promotion.request, screen.* |
contact | report.view, enquiries.view |
billing | membership.view, report.view, billing.pay |
The permissions:
| Permission | Allows |
|---|---|
profile.update | Changing one’s own details. Everyone signed in holds it; it cannot be taken away. |
organisation.update | Changing the organisation’s contact details |
listing.update | Editing the organisation’s documents within the tier’s member_editable rule |
photos.add, photos.remove | Adding and removing photos on a listing |
hours.update | Changing opening hours |
event.submit | Submitting an event |
people.invite | Inviting a colleague, in a role holding no permission the inviter lacks |
people.manage | Changing colleagues’ roles and removing them |
promotion.request | Asking for a promotion |
billing.pay | Paying for a tier or a promotion (paid listings) |
enquiries.view, enquiries.manage | Reading the enquiry inbox, and changing an enquiry’s status, note and fields |
report.view | The member report |
membership.view | The organisation’s memberships |
pending.view | Changes 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:
roles:
- key: marketing
label: { en-GB: Marketing }
can: [listing.update, photos.*, report.view, enquiries.view]
- key: contact
can: [report.view, enquiries.view, enquiries.manage] | Key | Meaning |
|---|---|
key | The role’s handle |
id | The 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. |
label | The role’s name per locale |
can | Permissions 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
portal:
mode: both
templated:
enabled: true
template_dir: portal
routes:
organisation: "/businesses/{org}"
invitations:
expiry: 30d
reminder_after: 7d | Key | Default | Meaning |
|---|---|---|
portal.mode | both | templated: 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.enabled | true | false stops the engine rendering screens in every mode |
portal.templated.template_dir | portal | The directory, under each template root, that holds your portal templates |
portal.templated.routes.<screen> | see Reference screens | A reference screen’s path under the portal path |
portal.invitations.expiry | 30d | How long an invitation link works |
portal.invitations.reminder_after | 7d | When an unaccepted invitation is sent a reminder |
portal.report.* | see Member reports | The 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.yaml | Default | Meaning |
|---|---|---|
auth.member.enabled | true | The member realm |
auth.member.magic_link.enabled | true | Sign-in links by email |
auth.member.magic_link.ttl | 15m | How long a sign-in link works |
auth.member.passkeys.enabled | true | Passkeys |
auth.member.password.enabled | false | Must stay false: member passwords are not built |
auth.member.session.absolute_lifetime | 720h | The longest a member session lasts |
auth.member.session.idle_timeout | 336h | How long an unused session lasts |
auth.member.registration | claim | claim: 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 key | Default path under the portal path | Shows |
|---|---|---|
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}/report | The member report; needs report.view |
contact_details | /organisations/{org}/details | The organisation’s contact details; organisation.update to change them |
people | /organisations/{org}/people | Colleagues, roles and invitations |
membership | /organisations/{org}/membership | Memberships by site; needs membership.view |
contact_team | /organisations/{org}/contact | A message to the destination’s team, recorded on the organisation’s timeline |
enquiries | /organisations/{org}/enquiries | The enquiry inbox; needs enquiries.view |
events | /organisations/{org}/events | The organisation’s events, and submitting one; needs event.submit |
promotions | /organisations/{org}/promotions | Promotion requests; needs promotion.request |
listing_edit | /listings/{id} | Editing a document within the tier’s rule; needs listing.update |
photos | /listings/{id}/photos | A listing’s photos |
hours | /listings/{id}/hours | A 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.
| Status | Meaning |
|---|---|
new | Not yet opened |
read | Opened |
replied | Marked as answered |
archived | Out 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.
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
| File | Holds |
|---|---|
config/memberships.yaml | The tiers |
config/sites/<site>/site.yaml memberships.tiers | The tiers a site sells |
member_editable in config/schema/<type>.yaml | What each tier may edit |
config/portal/roles.yaml | Partner roles |
config/portal/portal.yaml | The portal’s mode, templates, routes, invitations and report settings |
config/sites/<site>/portal.yaml | A site’s own portal, screens and actions |
config/portal/enquiries.yaml | Enquiry fields |
engine.yaml membership:, auth.member:, server.portal_path | Renewal 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.
CRM
Organisations, people and the links between them; custom fields on both; the timeline, tags and smart lists of organisations and people; broadcasts to member contacts; the CSV and XLSX import, merging, duplicates, trash and erasure; and every CRM command.
Portal screens and actions
How to give one site its own portal path, routes and hidden screens, declare portal screens with scoped data and portal actions that write documents or custom fields, restyle the reference portal's templates, and build a portal of your own on the member API.