BDShield
A twelve-plugin WordPress suite with a shared licence server and self-hosted updates.

BDShield is a suite of twelve WordPress plugins, sold and supported at getbdshield.com. Backups, an activity log, uptime monitoring, image optimisation, a firewall, and others.
This page is about the shared architecture underneath them, not about what any one of them does.
The constraint
Twelve plugins is not twelve projects. It is one project with twelve deployment targets, and the difference decides whether a small team can maintain it.
The second constraint is that the plugins run on hosting we do not control. The
production site itself taught us most of this: it runs LiteSpeed, where custom
HTTP headers set through .htaccess are stripped, so any feature that depended
on mod_headers had to be redesigned. ZipArchive is not available on that host,
so the backup plugin uses PclZip. Web PHP is 8.3 and CLI PHP is 8.5, which is
the kind of split that makes a cron job behave differently from the same code in
an admin request.
The stack, and why
PHP against the WordPress plugin API, with a deliberately narrow set of patterns repeated across all twelve.
Every plugin has the same skeleton. Constants for version and paths, a top-level
defaults function, activation and deactivation hooks registered at file scope, a
final singleton class with instance(), includes() and hooks(), and a
global accessor function. Settings live in one serialised array in wp_options
per plugin, read through a helper that merges with defaults. Tables are named
{prefix}_{plugin}_{feature} and created by dbDelta() in a static method
called from the activation hook. Admin screens are a singleton in
admin/class-*-admin.php with views in admin/views/.
None of that is clever. That is the point. Reading the fourth plugin costs nothing once you have read the first, and a fix found in one is mechanically applicable to the other eleven.
Three decisions
One updater file, copied not abstracted. class-bd-auto-updater.php is
byte-identical across the plugins. It hooks
pre_set_site_transient_update_plugins and plugins_api and checks a REST
endpoint on our licence server. We could have shipped it as a composer package
or a must-use plugin. We did not, because a shared dependency between twelve
independently installable plugins on somebody else's site is a support burden
we would carry forever. The cost is twelve copies to update in lockstep, which
is a scripted change. The benefit is that no plugin can break another one.
We host the licence server ourselves. bdshield-license is a WordPress
plugin on our own commerce site exposing POST /wp-json/bdls/v1/validate for
activation and deactivation, and a check-update endpoint the auto-updater calls.
Client-side, each plugin has its own licence class that caches the result in a
transient for a day and answers is_active(), get_tier() and
has_feature(). Rate limiting is 30 requests a minute per IP.
Gating is enforced at multiple points rather than one. In BD Auto Blog that is seven separate checks across the scheduler, the writer, the helpers and the admin screens. A single gate is a single line to comment out.
Deployment is a Python script per plugin. Each has a redeploy.py that
pushes over SFTP with paramiko, and every final build is copied to one canonical
production folder. This is not sophisticated and it does not need to be. What it
does is remove the step where somebody drags a folder into cPanel and forgets
one file.
What broke
The dark admin theme did not reach the edges of the screen. Thin white strips down the left and right of every plugin's dashboard.
Several changelogs already claimed this was fixed. It was not. The earlier
patches set margin: 0 on the wrapper, which does nothing about WordPress's
own #wpcontent gutters. One plugin was worse: it capped the element that
painted the background at max-width: 1200px, so on any wide monitor the theme
simply stopped.
The canonical fix pulls the painted wrapper into the gutter with a negative
left margin, sets a minimum height against the viewport, and puts the
max-width on the wrapper's children instead of the wrapper. One rule came out
of it and it applies well beyond WordPress: never put a max-width on the
element that paints the background.
We shipped that across all twelve in one pass, along with a single colour
palette replacing four different ones that had drifted apart, and a set of small
fixes each plugin had been carrying alone. A related one from the same sweep:
the speed plugin's HTML minifier was corrupting JSON-LD, because it did not
protect <script> and <pre> content before collapsing whitespace.
What we would do differently
Two things.
The scheduled work is inconsistent. Some plugins use wp_schedule_event() and
some use a self-rescheduling single event, and the choice was made per plugin
rather than once. Both clear their hooks on deactivation, so nothing leaks, but
a reader has to check which pattern they are in. That should have been one rule.
The other is the settings blob. One serialised array per plugin in
wp_options is fast to write and awkward to migrate, and every schema change
now needs a merge-with-defaults path that behaves correctly for a site that
skipped four versions. It was the right call at plugin one and it is a small
tax at plugin twelve.
Where it is now
All twelve are live and on sale at getbdshield.com, on one architecture, one palette and one updater. The suite is sold direct rather than through the WordPress.org repository, which is a distribution decision and not a technical one.
Built by BestDid
We build and maintain the BDShield plugins. Licences, tiers and documentation are at getbdshield.com.
Elsewhere on the shelf: the Toivo case study covers structured data discipline on a static site, and the XWipe case study covers building against a rate-limited third-party API. The full list is on our work page. To discuss a build, see services.