
Who Can Edit What: Owner and Manager Permissions
By menu-MENA Team
Published on August 29, 2026
Two roles, no permissions matrix
Open /admin/team as anything other than the account owner and the page bounces you straight back to the dashboard. That redirect is the whole story of how permissions work here: there isn't a settings screen with a grid of checkboxes for "can edit menu," "can view orders," "can manage billing." There's owner, and there's manager, and the type that backs both of them in the codebase is exactly two strings: 'owner' | 'manager'. Owner is whoever's Firebase uid matches the tenant's ownerId field. Manager is anyone the owner invited, full stop, no self-signup path exists for joining someone else's account.
What stays owner-only
A handful of pages and actions check for owner specifically, not just "someone with access." Team management itself. Billing and the subscription plan. The public API key. The custom domain setting. The audit log. Adding or deleting a branch. Copying a menu from one branch to another, or pushing a menu update out to multiple branches at once. And the aggregate analytics view that adds up scans and views across every branch in one chart. None of that opens up no matter how a manager's access is scoped, because the server checks the role directly on those requests, not the branch.
What a manager can actually do
Everything else on the branch they're scoped to: the menu editor, item prices, sold-out toggles, modifier groups, orders, reservations, coupons, theme and template settings for that branch's public page. The mechanism is one shared function, resolveEditableTenantFromCookie, that every write endpoint in the admin API calls before doing anything. It resolves who's asking and what tenant they're asking about, and returns either owner-with-no-restrictions or manager-with-a-branch-scope. A route that needs to be owner-only adds one line on top of that: if the resolved role isn't 'owner', reject with a 403. Everything else just runs against whatever branch scope came back.
One branch, or all of them
That branch scope is set once, at invite time, and it's either null (all branches under the account) or a specific branch's id. A manager scoped to null sees every branch in their branch switcher and can move between them freely, same as managing several locations under one owner. A manager scoped to one branch sees only that branch in the switcher, full stop, and even if they somehow got hold of another branch's tenant id, the server checks the requested branch against their stored scope on every single request and rejects anything that doesn't match. The branch picker also hides the "add a branch" and "manage branches" controls for anyone who isn't the owner, so a manager never sees an invitation to do something they can't actually do.
Sending an invite
The Team page has one button: Invite a member. It opens a dialog with an email field (optional, more on that below) and a branch scope picker that defaults to All branches. Submit it and the server generates a random 24-byte token, stores it in an invites collection with a 7-day expiry, and the link (/invite/{token}) gets copied to your clipboard immediately. If you did fill in an email, menu-MENA also sends it as a real email in the background, but that send is best-effort: if it fails, you've still got the link in your clipboard and nothing blocks on it.
Accepting, revoking, and removing
Whoever opens the invite link sees the restaurant's name and logo before they sign in, then claiming it is a single transaction: it checks the invite hasn't already been used or expired, checks the person isn't already a member, and only then creates their membership and marks the invite claimed. Two people can't race the same link into two memberships. From the owner's side, a pending invite can be revoked, resent (which quietly kills the old token and issues a fresh one), or copied again if the original clipboard copy got lost. An already-accepted invite can't be revoked, since there's nothing left to revoke, at that point removing the person happens from the members table instead, with an undo toast in case it was a misclick.
Every one of those actions, inviting, revoking, claiming, removing, writes an entry to the audit log with who did it and what changed. The raw invite token never gets stored there, only a fingerprint of it, because the token itself is a bearer credential and the log doesn't expire.