Back to all notes

Preparing an Established Freemium Plugin for Modern WordPress.org Review

A practical guide to modern plugin review, legacy compatibility, and the decisions AI coding assistants cannot safely make alone.

14 min read

Lately, I have been studying a pattern across the WordPress plugin ecosystem. WordPress.org is reviewing established freemium plugins with more automated checks and broader inspection of the full plugin package.

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 issues appear repeatedly. The free package contains premium code that only runs when Pro is active, or globally accessible names use prefixes that no longer meet current expectations.

For a new plugin, either issue may require a focused code change. 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 goal is not merely to clear warnings. The team must update the plugin without breaking existing sites.

Why Previously Accepted Architecture Can Fail Today

Many WordPress businesses grew around the same freemium structure.

The free directory plugin contained the shared foundation. 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.

Many established plugins passed earlier reviews with this structure. 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 the directory. 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 inside a function or class. The review issue concerns what ships in the free plugin and which names it exposes globally.

That distinction determines what must change and what should remain compatible.

Why Reviews Now Find More Issues

This shift is visible in the team’s own reports.

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. The developer documentation also describes an automated security review for new releases.

This does not mean every warning closes a plugin. The review process 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.

Past approval does not prove that a new release will pass today’s checks. The code may remain unchanged while the tools around it improve.

What Reviewers Check

A directory 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 must perform genuine work outside the plugin. They also need 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 traces data from input to output or action:

  • 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 Need Safe Migrations

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 JavaScript values shared with 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 saved data. A variable passed into a PHP template connects the caller with that template. A localized JavaScript object connects two separately built files. A namespaced class method is already protected by its namespace. A local variable cannot collide with another plugin.

Those things can share the same short prefix and still require completely different decisions.

A Prefix Change Is Not One Change

Prefix reports make this problem concrete.

Abstract diagram showing legacy plugin identifiers divided into three paths for coordinated runtime updates, careful data migration, and preserved local implementation details

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 change many files quickly. It would also ignore what each name represents.

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.

Stored identifiers need more care. Renaming a database key requires an explicit migration with clear rules for conflicting old and new values. 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.

Separate Free and Pro by Complete Features

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 restriction can expose a control that no longer works. Moving only the screen while leaving the business logic in Free still ships locked functionality through the directory.

Saved Pro settings create another risk. 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.

Plan for Free and Pro Updating at Different Times

WordPress does not update Free, Pro, and companion plugins at the same moment. 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:

ProviderConsumerQuestion
PreviousPreviousDoes the released baseline still behave as documented?
CurrentCurrentDoes the replacement work end to end?
CurrentPreviousCan Free update before the companion?
PreviousCurrentCan the companion update before Free?
CurrentAbsentIs the base plugin complete and non-fatal?
CurrentReactivatedDoes preserved data recover without duplication?

Sometimes neither update order is safe. That becomes a release decision. Do not quietly add broad compatibility aliases to the directory plugin to hide the risk. 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.

A Safer Review Workflow

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 tracking table for the findings. Each item records its source, file, entry point, affected data, whether the code can run, 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 old/new 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_options response 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.

Review the Release ZIP, Not Just the Repository

The submitted 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 contents. 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.

Turning These Lessons Into a Reusable Skill

The official documentation explains the policy well. The difficult decisions sit between a finding and a safe change. They include identifying the reviewed ZIP, classifying contracts, checking compatibility between versions, getting consent for migrations, assigning companion ownership, and separating scanner success from 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 primary starting point for current WordPress rules and recommended practices. Mine does not replace them.

The difference is practical. 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.