
The build nearly reached release with one word carrying two incompatible meanings: admin.
WPControl is a multi-site WordPress management SaaS for freelancers and agencies. It was built around a strict separation between the instance operator and each workspace's own administrators, with connected-site operations handled asynchronously. This account explains why clarifying that boundary became the decision that reorganized the final security pass.
Three project artifacts treated that role as the top of a workspace; one decision record treated it as an instance operator. The code implemented pieces of both readings, and the security phase finally forced a single answer.
Starting point
The initial product model looked simple: one workspace, several internal roles, and no client portal. Agency clients would receive reports rather than log into the application. The workspace itself would have an administrator, editors and viewers; a separate instance operator would control the platform-level surfaces. That distinction let the product keep daily site work close to the agency while reserving system configuration for the person running the SaaS.
The technical shape was less simple. A WordPress management operation can touch many remote sites, and a request that loops through them synchronously can outlive a serverless function. The build therefore adopted an asynchronous job model from the beginning: a bulk operation becomes a run with explicit pending, in-progress, completed or failed states. The platform fans work out instead of keeping one request open until every site responds. A reverse-poll path was also part of the decision record for hosts that block outbound calls.
The WordPress connector was deliberately treated as a separate track. The platform side would own signed requests, scoped secrets, revocation and staged updates; the connector would authenticate with a bearer credential plus an HMAC signature and retain an application-password fallback. Separating those tracks made the boundary legible, but it also created a real delivery limit: the consolidated build report later recorded the platform half as complete while the PHP connector itself was still absent.
The product's design went through a comparable reset. An earlier dark control-room direction had been rendered before a catalog-wide light-ground rule and before the behavioral markers used by the newer design process. The project did not patch that generation into compliance. It preserved it as superseded work, ran four fresh light-ground directions, and selected the dense multi-entity administration pattern. The reversal mattered because managing many sites is an information-density problem before it is a styling problem.
The decisions that shaped it
Authority belongs to a plane, not a label
The role model eventually settled on two planes. super_admin is the instance operator. admin, editor and viewer live inside a workspace, with admin as that workspace's ceiling. The important part is not the role names; it is that every invitation, promotion, API-key scope and navigation decision must preserve the same ordering.
That ordering also shaped the data layer. Project tables remain organization-scoped and protected by row-level policies even though the visible product model is a single workspace. The organization is an isolation mechanism, not a second product concept exposed to the agency. Self-registration therefore provisions a workspace invisibly, while an invited user joins the existing one with the assigned role.
Remote work is queued before it is executed
Bulk operations were designed around state transitions rather than optimistic buttons. A run can be inspected before remote work begins, and a held run contains no queued jobs. This creates one place to enforce safety whether the request originates in the interface, an automation or a connected tool.
The confirmation rule is concrete. A production write, a plugin or theme deletion, or an operation above the configured site threshold stops at a preview until it is confirmed. Dry runs do not create operation rows. Each decision is written to an action audit with whether it was a dry run, whether confirmation was required, whether it was granted and what outcome followed. The value of the rule is architectural: adding a new caller does not create a new path around the guard.
AI usage is measured where the provider reports it
The build record carried two silent-failure traps from the start. A generic refill helper reads a different limit key from the one WPControl uses, so calling it with a different credit label would still allocate nothing. A separate usage helper has no implementation. Neither failure produces the loud exception that usually sends an engineer to the right line.
The project added a dedicated refill path for AI-message allowances and wired it to both plan changes and monthly resets. Usage is charged from the provider's reported token event rather than an estimate, and state-changing or multi-step work is routed separately from trivial reads. The verification seeded a deliberately wrong balance before driving the real payment-event path, so an unchanged balance could not masquerade as a passing test.
The connector remains a supply-chain boundary
The connector plan includes per-site secrets, replay resistance, revocation, signed update packages and staged rollout. Those are not enhancements around the main feature. A central service that can update remote WordPress installations is a software supply-chain actor, so update failure and credential compromise belong in the original architecture. Keeping the PHP component on its own track made that responsibility explicit, even when it meant documenting that the overall connection path was not yet complete.
What fought back
The role vocabulary was the most consequential conflict because every local reading was plausible. One decision entry grouped admin with the instance operator. The sitemap, demo-account description and workspace helper treated it as a workspace role. The implementation split the difference: the workspace administrator was sent to the operator console, dozens of administration endpoints admitted that role, and three grant paths could elevate it to super_admin.
The security run turned the contradiction into a measured boundary failure. It found four unwaived critical issues and halted the remaining pipeline. Three cron endpoints accepted the literal credential form Bearer undefined and reached service-role work. The fourth was the missing role ceiling: a workspace administrator could attempt an operator-level grant through an invitation, a role change or an API-key scope. Database policies had passed because these HTTP routes used a privileged client that bypassed them by design. The SQL boundary was sound; the application boundary was not.
The resolution began with an operator ruling rather than a patch guessed from code. The decision record was corrected in place and retained its former wording and rationale. super_admin moved out of the workspace-assignable set. A shared comparison now checks the real actor before privileged writes, and the enforcement sits in services used by routes, connected tools and assistant actions rather than in one interface. The operator console became super_admin-only. Of the administration routes that had admitted workspace administrators, twenty-five were narrowed; three automation routes stayed workspace-level because their data and callers genuinely belonged there.
The verification deliberately tested the comparison itself, not only enum rejection at the edge. A workspace administrator was blocked from changing the instance operator to a lower role, while a legal workspace-level change succeeded and was rolled back. That test closed an adjacent hole the original finding had not named: acting on someone above the caller was as important as granting a role above the caller.
A second failure showed why green receipts need a denominator. During interface integration, the translation merger reported successful syntax and said it had consumed every input file, yet it silently dropped 345 rows. A route glob inside a SQL line comment contained /*; the scanner misread that text as the start of an unterminated block comment and treated everything after it as documentation. The visible symptom was one humanized label. Hand-fixing that label would have hidden the other lost rows. The parser was fixed instead, then the full set was merged and checked against the live rows.
Not every warning became an incident. The dedicated anomaly log still records no build anomaly; its entries are a watchlist of known traps rather than claims that those traps occurred. The narrative above uses only issues explicitly recorded in the progress and build reports. That distinction matters in a build story: a risk that was anticipated is not the same thing as a failure that was observed.
What shipped
The resulting build has a clearer internal contract than the one it started with. Workspace administrators operate their workspace; the instance operator retains the platform plane. Remote operations move through a queued state machine, high-impact actions stop for confirmation, AI allowances refill through their actual limit key, and the interface uses the light-ground, dense administration direction selected after the earlier design was superseded.
The record also keeps its boundary visible. At the consolidated reporting point, the platform-side connector routes, signed client and administration surfaces existed, but the PHP connector track did not. Buyer-facing documentation was changed so it did not imply that a site could already be connected. That is not a footnote to erase from the story: it is the difference between a platform prepared to coordinate remote work and an end-to-end connection path ready to perform it.
The phase measurements show where the work accumulated without pretending to provide a complete total. The run-state recorded 4,143 seconds for the backend phase, 885 seconds for the security phase and 5,201 seconds for the final skin and visual phase. The frontend swarm has no duration field, so this account does not infer one or present a full-build duration.
WPControl's build story is therefore not a sequence of feature completions. It is a sequence of boundaries made explicit: workspace versus instance, queued intent versus remote execution, measured usage versus silent defaults, and platform preparation versus connector delivery. The late role conflict earned the opening because it forced all four kinds of boundary to be stated in terms the code could enforce.
