Why a listing needs its own permission model
In a MAM directory app, a listing — a venue, a shop, a service provider — is more than a piece of content the app owner publishes. It’s something a real business wants to control. The venue owner expects to edit their own hours, post their own offers, message their own customers, and add their own front-desk staff, all from inside the app, without ever touching WordPress admin. Meanwhile, the directory owner needs to keep strangers from editing a venue they have nothing to do with, and needs an approval step before user-submitted listings go live.
That tension is exactly what the listing permission model solves: give each business real control over its own listing, while keeping everyone else out. It’s a small, per-listing role system that sits on top of the app’s global user roles. The same person can be an ordinary consumer browsing the directory one moment and the owner of one specific listing the next — the app shows them consumer buttons everywhere except on their own venue, where it swaps in the owner toolkit.
Here’s what this article covers: the four listing-scoped roles, what each one is allowed to do, and the two flows — claim and invite — that hand those roles out. It’s grounded in mam-geodirectory, where the model actually lives, and in mam-main‘s app-user manager, which stores the membership records as app_user_invite posts. (mam-chat-manager owns a related structure — the per-listing chat group — but that’s a different thing from the membership store.)
The four roles
Every time someone looks at a listing, the app resolves them into one (or more) of four roles — freshly, for that listing alone. That resolution lives in a shared trait (set_user_roles() in mam_gd_user_roles_trait.php), and every tab-bar button and content block on the screen checks it, so the answer never contradicts itself across the page.
| Role | How you get it | Where it is stored |
|---|---|---|
| Owner | You are the listing’s WordPress post_author. |
post_author on the listing post — not the invite system. |
| Manager | You hold an accepted manager invite for the listing. | An app_user_invite record in mam-main, sub-role manager. |
| Staff | You hold an accepted staff invite for the listing. | An app_user_invite record in mam-main, sub-role staff. |
| Admin | You are a WordPress administrator, and the request is in admin mode. | Your global WP role, gated by a request parameter. |
A few things in this table are easy to skim past, but they matter.
Owner is implicit and special. Ownership is never stored as an invite — it’s simply whoever the WordPress post author is. The runtime always treats that author as if they were a manager of their own listing (the helper that lists a listing’s managers automatically re-adds the author), so an owner never needs to invite themselves. There’s one exception: if the author is site staff — anyone WordPress lets edit other people’s posts (edit_others_posts, i.e. administrators and editors, including the listing-import system user) — that implicit owner-as-manager fallback is skipped. Those authors are seeding or managing listings on the site’s behalf, not running a venue, so they don’t get quietly promoted to its manager.
Manager and staff are mutually exclusive. A person is either a manager or staff on a given listing, never both — when staff are saved, anyone also in the manager list gets stripped out, so the two lists never overlap. Managers are the elevated members; staff are the regular ones.
Admin is the only role that is conditional on context. Being a WordPress administrator isn’t enough on its own — the admin powers only switch on when the request carries a pid parameter (the app’s admin/preview mode). Without it, an administrator browsing the directory sees the same consumer buttons as everyone else; they don’t get Edit Listing or Approve Listing scattered across every venue just because they happen to be an admin. That keeps admin-only actions confined to admin-mode views.
Here’s the mental model to hold onto: owner, manager, and staff are scoped to one listing. The same user can be an owner of listing A, a staff member of listing B, and a complete stranger to listing C — all in the same app session.
What each role can do
The whole point of these roles is to gate the action buttons on a listing’s detail screen. mam-geodirectory registers a large set of tab-bar buttons, and each one asks the role trait “who is looking at this?” before deciding whether to show itself. The buttons fall into two families, and the role flip is what swaps between them.
Consumer buttons vs. owner buttons
When you are not affiliated with a listing — not its owner, manager, staff, or an acting admin — you see the consumer action set: Favorites, Call, Website, Map, Email, Share, and (when chat is enabled) Chat. These are the things a customer wants: save it, call it, get directions, share it, message it.
The moment the app recognizes you as owner, manager, staff, or acting admin of that listing, it hides those consumer buttons and swaps in the owner toolkit instead. The thinking is simple: you don’t favorite your own venue or ask it for directions — you manage it. The skip rule that hides the consumer buttons covers website, email, phone, favorites, map, share, and chat, and it fires for owner, manager, staff, and admin alike.
The owner toolkit, button by button
Here’s who can do what, straight from the visibility checks behind each button:
| Action | Owner | Manager | Staff | Admin (in admin mode) |
|---|---|---|---|---|
| Edit Listing — change the venue’s own fields | Yes | Yes | No | Yes |
| Edit Staff — add/remove/role-change members | Yes | Yes | No | Yes |
| Send Notification — push to the venue’s audience | Yes | Yes | No | Yes |
| Edit Events / Edit Deals — manage events and offers | Yes | Yes | Yes | Yes |
| Approve Listing — publish or reject a submission | No | No | No | Yes (admin only) |
| Chat — see the listing’s chat group | (as member) | (as member) | (as member) | When non-admin staff exist |
Here’s the pattern worth internalizing: owners and managers share almost all the same powers. A manager is essentially a co-owner for day-to-day work — they can edit the listing, manage who else is on the team, and message customers. Staff have a narrower, day-to-day slice. Being staff puts you inside the venue’s world — you are part of its chat group, you see the listing as “yours” so the consumer buttons disappear, and you can post the venue’s events and deals. What staff cannot do is the structural work: they cannot edit the listing’s own fields, add or remove other members, or send push notifications. Staff are the front-desk people who keep the calendar and offers current, not the people who restructure the listing or manage the team.
Approval is admin-only by design. No listing-scoped role can approve a listing. Approving a user-submitted listing flips it from pending/draft to publish (or, on reject, deletes it outright) and notifies the author. That’s a gate the directory owner keeps for site administrators — never the venues themselves.
Two quiet limits to know about
Two of the owner buttons carry caps that come from the listing’s pricing package, and both fail silently:
- Send Notification is throttled by a monthly push quota (
pn_per_monthon the package, default 10). Once a venue hits its quota for the month, the button simply disappears until the first of the next month. - Edit Staff is capped by a maximum staff count (
no_of_staff_memberson the package). Once the listing is at its limit, the “Add Staff Member” option drops off — existing staff can still be edited or removed, but no new ones can be added until someone leaves.
Neither limit explains itself on screen, so it’s worth surfacing these caps in your in-app onboarding — that’s how you head off a stream of “the button vanished” support tickets.
How roles get assigned: the two flows
Roles don’t appear by magic — there are exactly two ways a person becomes an owner, manager, or staff member of a listing, claiming and inviting, plus the admin shortcut of editing membership directly in WordPress.
The claim flow — becoming the owner
Claiming is how a business takes ownership of a listing that already exists in the directory but isn’t theirs yet. Think of a venue that the directory owner seeded, or that another user submitted — the real business comes along and says “this is mine.”
The Claim Listing button is a small state machine, and it shows one of three faces depending on where you are in the process:
- “Request to Claim” — shown when the directory is configured to require approval first. Tapping it files a pending claim request (with the user’s name and the comments they wrote) and waits.
- “Claim Pending” — shown while a request is sitting unapproved. It’s informational; there’s nothing to tap.
- “Claim Listing” — the ready-to-buy state. Tapping it opens a purchase screen (Stripe or in-app purchase, depending on configuration) showing the available pricing packages, a call-to-action, and a feature list.
Whether approval is required is a directory-level setting: turn it off, and a user goes straight to the purchase state; turn it on, and an administrator has to approve the request before the button advances to purchase.
When the purchase completes, the underlying GeoDirectory claim primitive does the real work: it marks the listing claimed and re-points the listing’s post_author to the new owner. That single change is what makes the permission model recognize them — because owner is post_author. From that moment, the new owner sees the owner toolkit on their listing and the consumer buttons disappear.
The claim button never appears to someone who is already the author, to a signed-out user, on a listing that is already claimed, or on a post type that doesn’t support claiming.
The invite flow — adding managers and staff
Once someone owns (or manages) a listing, they build their team through invites — two ways to send them, one way to respond.
From inside the app — the Edit Staff button. An owner or manager taps Edit Staff and gets a list: an “Add Staff Member” form plus one editable entry per existing member. They fill in a name, email, and a role of Manager or Staff. Submitting creates (or updates) an app_user_invite record in mam-main with the chosen sub-role, and fires a push notification to the invitee. The same form is how members get their role changed, reinvited, or removed.
From WordPress admin — the Staff & Managers meta box. Every listing’s edit screen carries a Staff & Managers meta box with two searchable fields, Managers and Staff. This is the admin path: useful for bulk onboarding, fixing a botched invite, or seeding test data. Saving the post clears the previous lists and re-applies the selections as accepted invites. (Because the author is the implicit owner, you never need to add them to Managers — and, as above, an administrator author is left out of the implicit owner-as-manager fallback.)
Accepting — the Manage Invites form. An invite is a pending membership until the recipient says yes. When a user with a pending invite next opens the app, the app automatically shows them a Manage Invites form on launch — venue name interpolated into the message, plus an Accept / Decline choice. It is not behind any button; it is pushed as the app’s initial form. Until they accept, they are not yet a manager or staff member, and they will not show up in the membership lists. (Only one pending invite is offered per launch; the next launch offers the next.)
What acceptance triggers downstream
Accepting an invite is more than a cosmetic flag. Acceptance runs through mam-main‘s update_user_invitation_to_post_id, which flips the invite’s invite_status to accepted and — only on a real status transition — fires the invite-lifecycle hooks mam_app_user_invite_status_changed and the status-specific mam_app_user_invite_accepted (with _declined / _revoked mirrors). Those hooks are the intended seam for downstream reactions such as enrolling the new member in the listing’s chat group (a mam-chat-group post that mam-chat-manager maintains, keyed to the listing). In practice, from the moment an invite reads accepted the person shows up in the listing’s membership queries, which is what lets managers and staff message each other and the owner about the venue.
How this connects to the rest of the suite
The listing permission model touches several MAM plugins at once — a good sign that it’s core to how a directory app behaves, not a side feature.
-
mam-main’s app-user manager is the source of truth for manager and staff membership. The roles aren’t stored in
mam-geodirectoryat all — they live asapp_user_inviteposts (keyed bymain_post_id,sub_role, andinvite_status) thatmam-mainowns, read and written through a small set of filters (mam_get_group_members_for_post_id,mam_invite_app_user_to_post_id, and friends). mam-chat-manager owns a separate but closely related structure — the per-listing chat group — so that owners, managers, and staff are exactly the people who can message each other about a venue, and so a consumer’s Chat button only appears when a venue actually has staff to receive the message. -
Pricing packages (via the GeoDirectory pricing add-on) set the limits the roles operate under — the monthly push quota and the staff cap both come from the listing’s package, and the claim flow sells those packages. The permission model decides who can act; the package decides how much they can do.
-
The notification system is what the Send Notification button feeds, what the staff-invitation push rides on, and what tells an author their listing was approved or rejected. Several role-gated actions exist precisely to produce a notification.
-
The app’s global role-and-audience model sits underneath all of this. Listing roles are a per-listing overlay; the global roles (and the
pidadmin-mode gate) decide what a person can do across the whole app. The two compose: a person’s global role plus their role on the specific listing in front of them together determine the buttons they see.
If you take one idea away, let it be this: on a MAM listing, “who can do what” is decided per listing, every time, by resolving the viewer into owner / manager / staff / admin — and the claim and invite flows are simply the two ways those roles get handed out.
Related
- Recipe: Manage staff and managers (admin) — the WordPress meta-box path for assigning roles
- Form: Add or remove staff and managers — the in-app Edit Staff flow
- Form: Manage staff invitation — the auto-displayed Accept/Decline form
- Form: Approve Listing — the admin-only approval gate
- Form and flow: Claim Listing — the request → approve → purchase ownership flow
- Listing tab bar buttons — the full button-by-button visibility reference
- Integration: Chat Manager — how membership becomes the per-listing chat group
- Concepts: User roles and what each role sees in the app — the global role model this overlays
