Preparing an Established Freemium Plugin for Modern WordPress.org Review
A practical guide to modern WordPress.org review, legacy compatibility, and the decisions AI coding assistants cannot safely make alone.
Lately, I have been studying a pattern across the WordPress plugin ecosystem. WordPress.org is reviewing established freemium plugins with broader automation and deeper package inspection.
The Plugins Team temporarily closes some plugins while their teams respond to a review. It pauses new releases for others. The public explanation is often short, but two architectural themes appear repeatedly. The free package contains premium functionality behind a Pro condition, or public identifiers use prefixes that no longer meet current expectations.
For a young plugin, either issue may require a contained refactor. For an established product, the same change can affect saved shortcodes, options, custom roles, post types, database records, page builder content, external integrations, and separately released add-ons.
I have been dealing with that part lately. It deserves more attention than a scanner report normally gives it.
Speed matters when a directory decision affects discovery, new installations, support load, and revenue. Care matters because the quickest correction can damage the upgrade path for people who already depend on the plugin. The engineering problem is larger than making warnings disappear. The team must change the distributable without casually rewriting its history.
The Architecture Passed Before
Many WordPress businesses grew around the same freemium structure.
The directory plugin carried the shared product. Pro added more settings and behavior. Sometimes the free package also contained parts of the premium implementation, then checked for a license or a Pro function before enabling it. Admin screens showed disabled controls, upgrade labels, or empty sections when the add-on was absent.
That architecture passed earlier reviews for many established plugins. An earlier approval does not guarantee that every part of the package will pass the current process.
The Plugin Directory Guidelines are clear about trialware. Users must be able to use the code distributed through WordPress.org. A paid add-on can provide additional functionality from outside the directory. A genuine service can charge for work performed on its servers. A license check cannot unlock functionality already present inside the free package.
Public naming has a similar history. Two- and three-character prefixes are common in older plugins. They are easy to type and often derived from the product name. Current WordPress plugin best practices ask globally accessible code to use a distinctive prefix with at least four letters, with five recommended.
The premium business model is still valid. Short internal names are still valid in local scope. The review problem appears at the package boundary and the global runtime boundary.
That distinction controls the whole remediation.
The Review Machinery Reaches Further Now
This pattern is not based only on support groups and directory pages.
The Plugins Team has documented the expansion of its tooling. Its 2024 review report said the internal scanner had received more than 400 commits and contained over 200 checks. The team reported finding more issues per plugin through automated and manual review.
The Plugin Check roadmap goes further. It describes plans to expand checks to updates from plugins already in the directory, including checks after a release. WordPress.org also documents an automated security review for new releases.
This does not mean every warning closes a plugin. WordPress.org distinguishes between a blocked release and a closed plugin. A blocked release leaves the previously distributed version available. A closure is a separate action and is communicated separately.
Historical approval is therefore a weak baseline for a new submission. The code may remain unchanged while the tools around it improve.
What They Look At
A WordPress.org review reaches far beyond whether PHP parses.
The team examines licensing for your code and bundled dependencies. Generated JavaScript and CSS need maintained source and build instructions. External services need a real service boundary, clear disclosure, and appropriate consent. Remote executable files, private update systems, bundled Core libraries, excessive admin notices, direct file access, and release metadata also matter.
A security review generally follows data from entry to effect:
- Where did the request originate?
- Is the operation public, private, or able to change data?
- Does it use a nonce for that action where appropriate?
- What capability permits the operation?
- May this user access this particular booking, listing, file, or setting?
- Which fields are accepted, and what types are valid?
- How is the result escaped in its final HTML, attribute, URL, JavaScript, CSS, or file context?
A nonce does not answer all of those questions. It verifies intent within a request flow. Authorization still needs to establish whether the requester may perform the operation or access the requested record.
The common issues guide also says its examples are not exhaustive. When an email names one unsafe request, one short AJAX action, or one generated bundle, I use that example to find the pattern across the entire release. I do not stop after changing the named line.
Existing Sites Turn Cleanup Into Migration
A new plugin can rename a shortcode before anybody saves it. An established plugin cannot pretend its database is empty.
Users may have the old shortcode inside posts, widgets, builder data, reusable templates, imports, and options. Renaming the registration alone makes the source look compliant while old pages lose their output.
The same applies to options, metadata, roles, capabilities, custom post types, taxonomies, table names, and browser contracts consumed by an add-on. These values survive longer than the function that first created them.
For every identifier, I now ask what kind of contract it represents.
An AJAX action is a public runtime contract. A value stored in post_type is persisted data. A variable passed into a PHP template is an include contract. A localized JavaScript object connects two separately built files. A namespaced class method is already contained by its namespace. A local variable has no global collision surface.
Those things can share the same short prefix and still require completely different decisions.
A Short Prefix Was Never One Change
Prefix reports make this problem concrete.

The same legacy prefix can identify runtime contracts, persisted data, or local implementation details. Each category requires a different decision.
An older plugin may use the same two- or three-character prefix across public AJAX actions, hooks, constants, option names, post types, metadata, local variables, class methods, script data, and templates. A global replacement would produce an impressive diff. It would also erase the meaning of each use.
Public functions and actions need a longer, unique prefix. Their consumers must move with them. That means checking PHP registrations, JavaScript source, committed bundles, nonce names, direct callers, and companion plugins.
Persisted identifiers require restraint. Renaming a database key requires an explicit migration with defined conflict behavior. Existing data may contain both the legacy and replacement keys. Users may import an old backup after the plugin has saved a migration marker. Some installations skip multiple versions before updating.
Local variables did not need a compliance rename. Neither did method names protected by a namespace merely because their basename started with tf_.
The scanner provides the search term. It cannot classify those contracts for the developer.
Free and Pro Must Separate by Behavior
Moving a premium field out of the free plugin sounds simple. It rarely is when that field participates in a booking.
The UI may live in one file while its default value is read in another. Pricing may consume it later. Imports and exports may know the same key. Templates can branch on its saved value. Checkout, order details, email, REST responses, and availability calculations may all carry part of the feature.
Removing only the setting creates an empty tab. Removing only the visual gate can expose a control whose processing path is missing. Moving the renderer while leaving the business logic in Free still ships locked functionality through WordPress.org.
Legacy metadata adds another edge. A site may have saved a Pro-only mode while the add-on was active. When Pro is disabled, Free needs a complete fallback. It must preserve the premium value so reactivation can recover it, but it cannot let that value select a blank or non-bookable path.
Separating one feature at a time is safer than using one large “move premium code” branch. For each feature, I trace its fields, defaults, validation, storage, processing, templates, client code, imports, API output, and deactivation behavior. The free plugin must work alone before I combine its current version with the current Pro version.
That takes longer per field. It creates much less uncertainty per release.
The Mixed-Version Window
WordPress does not update a plugin family as one transaction. Free may update before Pro. An add-on may update first. A customer may deactivate one plugin during troubleshooting and reactivate it days later.
Every shared change needs a compatibility matrix:
| Provider | Consumer | Question |
|---|---|---|
| Previous | Previous | Does the released baseline still behave as documented? |
| Current | Current | Does the replacement work end to end? |
| Current | Previous | Can Free update before the companion? |
| Previous | Current | Can the companion update before Free? |
| Current | Absent | Is the base plugin complete and non-fatal? |
| Current | Reactivated | Does preserved data recover without duplication? |
Sometimes neither mixed direction is safe. That becomes a release decision. Broad compatibility aliases should not be hidden in the directory package to avoid making it. The team may need a coordinated maintenance window, a backup notice, and a clear supported update order.
Fresh-install testing will not reveal this. The risky state lives between versions.
AI Can Make the Wrong Change Very Quickly
An email with dozens of findings is exactly the kind of task people hand to an AI coding assistant.
I use one too. It can search thousands of files, compare companion repositories, build an inventory, and prepare focused regression tests faster than I can do the mechanical work manually.
The planning stage changed how I use it. Before any implementation began, I asked the assistant to investigate the findings and plan the remediation. Parts of that plan were startling.
It proposed broad prefix replacement without first separating global functions from persisted post types. It considered applying manage_options to permission warnings, which could have blocked legitimate editors, store managers, vendors, or custom roles. It treated an old migration as removable because the current development database no longer appeared to need it.
Other suggestions were equally plausible on the surface: remove Core includes as a category, rename shortcode registrations without tracing saved content, separate a Pro interface without examining the fallback selected by legacy metadata, or escape download bytes because an output scanner reported an unescaped variable.
Those ideas never reached a customer site. The planning step did exactly what it should. It exposed the assistant’s assumptions before they became code.
That was the useful warning. A polished plan can still be built on an incomplete model of an established product. Each recommendation has to be checked against runtime scope, stored data, companion releases, user roles, upgrade history, and the exact policy claim it is meant to answer.
A controlled review process turns unsafe proposals into rejected options, regression tests, and durable engineering rules.
AI is useful inside a controlled boundary. It should not decide whether customer data can be rewritten, whether a public integration may disappear, or whether an organization has consented to a destructive migration.
My Working Rules
I start with the artifact that was actually reviewed. I record the Git commit when it is known, preserve the reviewer email, and calculate the SHA-256 of the submitted ZIP. If the report has no revision or package identity, I mark that gap instead of pretending the current branch is identical.
Then I build a finding ledger. Each item records its source, file, entry point, affected data, reachability, severity, confidence, policy reference, consumers, and remaining proof. I keep human instructions, scanner output, AI comments, and QA interpretation separate.
Before implementation, I:
- Turn every confirmed example into a pattern-wide search.
- Compare the candidate with the released baseline, including changes to stored data.
- Trace every maintained Free, Pro, add-on, theme, PHP, and JavaScript consumer.
- Classify public names separately from persisted identifiers and local scope.
- Decide which actions require approval from the product or data owner.
- Write the previous/current compatibility matrix.
- Define how the exact release package will be built and tested.
I refuse several shortcuts now:
- No global replacement of a short prefix.
- No warning suppression presented as remediation.
- No blanket
manage_optionsresponse to authorization findings. - No deletion of user data, legacy keys, or migration code without explicit authority.
- No UI-only Free/Pro split.
- No “smoke test passed” claim based only on a clean installation or screenshot.
- No release readiness statement based on the source tree when nobody has inspected the submitted ZIP.
I implement the work in small, reversible chunks. I keep a request security change separate from a shortcode migration. I move a premium feature as one complete behavior. Tests cover the valid flow, unauthorized access, malformed input, repeated migration, missing companions, and credible legacy states.
The ZIP Gets Its Own Review
The WordPress.org package is the product users receive. The Git repository is only one source for it.
Before calling the code ready, I build the ZIP with the real release procedure and inspect its inventory. The archive needs one correct top level plugin directory, the expected main file, consistent version metadata, readable source or a public source link, and the production dependencies that the plugin uses.
The package should not carry debug logs, local agent instructions, audit reports, proof screenshots, temporary fixtures, nested archives, editor files, secrets, or abandoned dependency trees.
I extract that ZIP into a clean directory and run Plugin Check, PHPCS/WPCS, source scans, and integrity checks there. Then I install the same archive in an approved WordPress environment with WP_DEBUG enabled.
This is where several false confidence signals disappear. A clean repository does not prove a clean ZIP. A successful PHP lint does not validate readme.txt. A screenshot proves the rendered state that was captured. It says nothing about the upgrade path that was never executed.
I Wrote the Guardrails Down
The official documentation explains the policy well. The difficult decisions sit between a finding and a safe change. They include artifact identity, contract classification, compatibility between versions, migration consent, companion ownership, and the difference between scanner success and release evidence.
I collected those rules in a reusable Codex skill called WordPress.org Compliance.
The skill gives an AI agent a stricter job. It freezes the evidence, separates findings by confidence, traces public and persisted contracts, requires explicit authority around user data, and checks the exact distributable. Its inventory script does not change files. It labels every match as a candidate.
WordPress also maintains official agent skills, including guidance for plugin development and the Plugin Directory. Those skills should remain the canonical starting point for current WordPress rules and recommended practices. Mine does not replace them.
The difference is operational. The official skills explain what WordPress expects. This skill helps a team apply those expectations to a specific release. It asks which findings are proven, which public or stored contracts are at risk, whether separately updated components remain compatible, and whether the exact ZIP has enough evidence for a submission decision. A straightforward new plugin may need only the official guidance. An established product, a reviewer response, or a high risk update can benefit from both.
That approach follows WordPress’s own principle of small, composable skills. The official skills carry policy knowledge. This skill adds a release process that applies it without unsupported assumptions.
It also refuses to promise acceptance. The Plugins Team performs an independent review, and its own common-issues documentation says the published examples are incomplete.
If the script finds a suspicious line, somebody still has to understand what that line means before changing it.
That is the central lesson. Use AI for speed, but keep evidence, compatibility, and consent in human hands. The goal is not to silence a report. The goal is to ship a compliant update without surprising the people who already use the plugin.