Core platform. Always on — this is how every WebProCMS install models people. There is no toggle.
Every person on a WebProCMS site — the owner, staff, and every visitor who signs up to save a listing, buy a product, enroll in a course, or use the client portal — is a row in the users table with one login. There is no separate "members" account system: what was once a members table was merged into users (2026-07), and the paid Memberships addon is just one more consumer of the shared account system, not the owner of it.
Roles: account holders vs. staff
| Role | Who | What they see |
|---|---|---|
| Standard | Everyone who signs up on the public site (home buyers, shop customers, students, portal clients) | The front end only — the site's own theme, plus the account area. Standard users who hit a /dashboard URL are redirected to the account hub; they never see the CMS. |
| Manager | The lowest staff tier | The CMS dashboard, gated per section |
| Admin / Super | Site administrators | Full CMS |
Public registration always creates Standard users. Staff are promoted from Dashboard → Users, which lists every account with search, a role filter, and pagination — "people who signed up" are simply users at the Standard role (the member-specific view at Dashboard → Memberships → Members shows the subset with subscription/licensing profiles).
Creating a staff account: invite, or type a password
New User defaults to emailing an invite rather than asking the admin to invent a password for somebody else. The account is created with a throwaway secret nobody ever sees, and the emailed link is the only way in until the invitee chooses their own — so the password never travels through an inbox or a chat window, and never lands in a logfile.
Two details worth knowing:
- The invite link is good for seven days, not the one hour a password reset gets. It has its own broker (
auth.passwords.user_invites) and its own accept route (/set-password/{token},UserInviteController) for a mechanical reason: a token's lifetime is enforced by the broker that validates it, so accepting invites through Fortify's reset route would have killed them after an hour however they were minted. Resend invite (in the row menu) mints a fresh link, which also revokes the old one — that covers a bounce, an expiry, or a mistyped address. - The toggle disappears on an install that cannot send mail, replaced by a notice and the password field. This is checked before offering the option, never after sending, because the
logtransport accepts every message and reports success: an unguarded invite would tell the admin it went out while the new account sat unreachable. Set mail up at Settings → Email.
Accounts created either way are marked email-verified on the spot — verified gates every dashboard route, so an account that landed unverified could not use its own invite.
The account area is front-end, the CMS is staff-only
Account holders are served the site's own front-end template — site header, page-title band, account navigation — never dashboard chrome. Staff browsing the public site get the same header links as everyone else (see below) plus the floating bottom-right admin control (Edit Page, New Page, Dashboard, quick actions), which is their door into the CMS.
Neutral URLs — headers never hardcode a module
Header designs link two URLs that resolve per install and per visitor:
/account/login— the login link. Resolves to the member login when the account system is live on the install (any of Memberships, Ecommerce, Real Estate, Courses, or Client Portal enabled — seeApp\Support\MemberAccounts), else the staff login./account— the My Account link. Managers+ go to the CMS dashboard; every other logged-in account goes to their account dashboard; guests go to the login.
Header rows therefore have exactly two auth states: guests see Log In, any logged-in account sees My Account. There is deliberately no Dashboard link in public headers — staff experience the site exactly as visitors do.
The subscription/licensing half: member_profiles
Identity (name, email, password, verification, 2FA, passkeys) lives on users. Accounts that came through the member system additionally carry a member_profiles row: subscription status, Stripe customer/subscription ids, dunning state, license key, install last-seen telemetry, and the seat-team owner link. A profile is created automatically the first time an account is used as a member (public signup, checkout, licensing). Staff accounts get one too the moment they do member things — the site owner saving a listing is one account doing both jobs.
Plan gating and member business logic resolve through MemberAccounts::current(): null for guests and for accounts with no profile (no profile = no plan).
For existing installs (the merge migration)
Installs updating across the merge run a one-time data migration: every members row is folded into users by email (case-insensitive; collisions attach the profile to the existing user — an owner's admin + test-member accounts become one), all member_id foreign keys are remapped to user ids and repointed at users(id), member passkeys move to the shared passkeys table, and every session is invalidated (old member sessions stored pre-merge ids). Everyone logs in once afterward. The migration is idempotent and a no-op on installs with no members.