Skip to main content
Back to Blog
wordpress plugin-development members open-source access-control

How I Built Selective Role Reset for the Members Plugin

· 4 min read

If you’ve spent any real time with the Members plugin, you’ve probably ended up with a roles screen that looks nothing like where you started. Custom roles you created for testing, capabilities added and removed across multiple roles, default roles modified to fit a specific permission model — and at some point, you want a clean slate.

Until now, getting back to that clean slate meant manual work. Delete each custom role one by one. Manually restore default capabilities by hand or by re-running activation. Hope no users got orphaned in the process.

The new Reset Roles feature in the Members plugin does this in one click — and the engineering work is in what it deliberately leaves alone.

Why a Naive Reset Doesn’t Work

The obvious implementation is: delete all non-default roles, restore the five core WordPress roles to factory defaults, done. It takes about ten lines of code, and it would ship a feature that quietly breaks production sites.

Here’s why:

Other plugins create roles too. WooCommerce adds customer and shop_manager. Easy Digital Downloads adds shop_accountant and others. LearnDash, BuddyPress, MemberPress itself — most ecosystem plugins extend the role system. A naive reset would wipe all of them, silently.

Users get orphaned. If you delete a custom role without reassigning its users, those users keep a role reference that no longer exists. They lose all capabilities — including login, in some cases. Recovery is manual.

Default role capabilities drift. Site owners may have modified default roles (Editor, Author, Contributor) by adding or removing capabilities. A reset has to actually restore those to factory state, not just leave them as-is.

A “Reset Roles” button that ships with any of these problems is worse than no button at all, because it gives users a destructive action they can’t reason about.

The Solution: Track What Members Creates, Reset Only That

The implementation is built around one architectural decision: the plugin tracks every role it creates, and only those roles are eligible for reset.

Tracking on creation and deletion. When a user creates a role through Members’ UI, the plugin tags it as a Members-created role. When they delete one through Members, the tag goes too. Roles created by other plugins, or by code outside Members, are never tagged — and therefore never touched by reset.

This is exposed through three small functions: members_get_created_roles(), members_track_created_role(), and members_untrack_created_role(). Other plugins or custom code can integrate with the tracking system if they need to.

Safe user reassignment. Before any custom role is deleted, the plugin reassigns its users to the default role defined in WordPress’s General Settings. No user ends up with a dangling role reference. No capability gets silently revoked.

Factory restoration via populate_roles(). For the five core WordPress roles, the reset uses the WordPress core function populate_roles() — the same function WordPress runs during installation. This guarantees the result is identical to a fresh install. No drift, no missing capabilities, no edge cases from maintaining a manual capability list inside the plugin.

Administrator capability protection. The reset explicitly verifies that the Administrator role retains all of its core capabilities after the operation. If anything is missing, it’s restored. The user running the reset can’t accidentally lock themselves out by triggering it.

The Smaller Details That Matter in Production

A few things that don’t show up in the feature description but matter:

CSP-safe spinner. The original spinner implementation relied on inline JavaScript, which trips Content Security Policy headers on hardened WordPress installations. I refactored it to use a pure CSS spinner toggled via class — same UX, no CSP violations.

Cache-busted assets. Admin scripts are enqueued with filemtime() as the version query arg, guarded by file_exists(). On every code change, the browser fetches the new version. No more “I updated the plugin but the old JS is still running” support tickets.

Confirmation copy. The reset triggers a confirmation dialog that explicitly says what will and won’t be deleted, and the success message describes what actually changed. When the action is destructive, the words on the screen need to make the scope unambiguous.

Why This Matters

The principle behind this contribution is the same one I apply to any production code: destructive actions must be precise about what they destroy.

A reset that wipes everything is not a reset — it’s a reinstall. A reset that wipes only what the plugin itself created, reassigns users safely, and restores defaults to factory state is a tool a site owner can use without a backup, without a staging environment, and without calling support.

The feature is live in the latest release of the Members plugin.

Have a similar challenge?

I help SaaS companies and WordPress platforms solve their most complex technical problems.

Let's Talk