Four layers must agree
Members-only content is protected by a chain: the membership plugin's rule on each item, the level each user holds, any cache that stores a copy of the page, and every other route by which the same content can be requested. Testing in the admin as an administrator tests none of them properly, because administrators usually see everything. A reliable check names the account types, names the items, states for every pair whether access should be allowed or denied, and then tests each pair from a clean browser profile. That table is the access matrix, and it is the only artefact that lets two people agree on what "fixed" means.
- Rows: logged-out visitor, each level, an expired member, an administrator.
- Columns: each protected item, including downloads.
- Cells: expected and actual result.
Caches are the usual cause of strange access
A membership vendor's caching guide states the problem directly: cached protected content can be seen by non-members, and members can be left unable to view what they paid for. Its advice is to exclude the plugin's own pages and the log-in and password-reset pages from the cache, and either disable caching for logged-in users or exclude all member-protected content. It also says payment gateway webhook addresses must stay uncached, that some hosts cache only for frequent callers so the address looks fine when you test it yourself, and that after changing restrictions you may need to clear the object cache. This is one vendor's documentation for its own plugin, so check your plugin's own guidance, but the pattern applies broadly: test with the cache on, as it is live, and with sequences such as logged-out, member, logged-out.
- Ask your host which cache layers exist; many hosts cache without any plugin.
- Staging without the live cache can pass while live fails.
Other routes to the same content
WordPress exposes content through more than one route: archives, search results, feeds and a data interface called the REST API. The posts reference says a list request defaults to published posts, that a context parameter takes view, embed or edit with view as the default, and documents a password parameter for password-protected posts. It does not say who may use the edit context, and it does not describe what a membership plugin changes, so the safe course is to test rather than assume. The authentication handbook explains that cookie authentication needs a nonce, and that without one the current user is set to zero and the request is unauthenticated, which is the situation of a logged-out visitor. Application passwords, available since WordPress 5.6, are for remote clients over https.
- As a logged-out visitor, request the data-interface address of one protected item and read the response body.
- Search for a protected title while logged out and see what the results show.
- For downloads, request the file address itself and report what happens.
Role changes made in code may not apply
The roles and capabilities handbook has a detail that explains many staging-versus-live differences. Roles are stored in the database, and after the first add_role() call the role is saved there; later calls do nothing, including attempts to change its capabilities. Changing capabilities in bulk means removing the role and adding it again, which the handbook warns should be done only when the capabilities differ because of the performance cost. A capability change deployed in code can therefore have no effect on a site whose database already holds the role. Check what the database holds, not only what the code says.
- Compare the capabilities of each role on staging and live.
- Treat "works on my copy" with suspicion if the copy came from a different database.
Fit, limits and the paid job
This guide cannot show that nothing leaks; it shows where to look. The access job has a published test price of GBP 395 for one membership plugin, up to four levels and ten items, on a staging copy with made-up accounts. It is accepted when every cell of the matrix passes, a cached sequence returns the right result each time, other routes show no restricted body text for a logged-out request, and the live views match after your site holder applies the settings. It does not repair payment-to-access sync, switch plugins or give legal advice, and it cannot recall copies others already hold. Prices are untested proposals and payment follows the agreed checks.
- If members paid but their level did not change, the fault is in the payment sync.
- If confidential material is exposed now, take it offline first and fix it second.
Sources and limits
- Paid Memberships Pro documentation: caching Checked 2026-10-11.
- Caching protected content can show it to non-members and can stop members seeing what they paid for; exclude the plugin's pages, log-in and password-reset pages from the cache; disable caching for logged-in users or exclude member-protected content; payment gateway webhook addresses must stay uncached; the object cache may need clearing after restrictions change.
- WordPress REST API handbook: posts reference Checked 2026-10-11.
- A list request defaults to the publish status; the context parameter takes view, embed or edit and defaults to view; the password parameter is documented for retrieving password-protected posts; the page does not say who may use the edit context.
- WordPress REST API handbook: authentication Checked 2026-10-11.
- Cookie authentication needs a nonce, and without a nonce the current user is set to 0 and the request is treated as unauthenticated; application passwords have shipped since WordPress 5.6 and are sent as Basic Auth over https.
- WordPress plugin handbook: roles and capabilities Checked 2026-10-11.
- Roles and capabilities are stored in the options table under user_roles; after the first add_role() call the role is saved and later calls do nothing, including attempts to change capabilities; changing them in bulk means remove_role() then add_role().