The subscribe.php handler runs eight stages before it touches the database: method check, origin check, per-IP rate limit, honeypot and time-trap, email validation, DB upsert, then confirmation mail. Regardless of what happened in the upsert, the caller gets an identical response body. Whether the address already existed, whether it just enrolled, whether it failed validation: the message is the same. That is the enumeration-resistant part.
The host is a cPanel shared server, with no Docker, no Composer, and no autoloader. The six library classes that back this pipeline load via plain require_once. The whole backend lands in around 600 lines across 15 files: schema, endpoints, .htaccess, and an ops runbook.
The constraint is instructive.
Why no Composer
cPanel shared hosting is not a development environment. It is a deployment target with PHP and MySQL, a file manager, and a database admin panel. There is no CLI access reliable enough to run composer install on deploy. Committing vendor/ is an option; it is also a multi-megabyte commit for a backend that needed six class files.
Composer is correct when you have external dependencies. When you do not, it is ceremony.
Six classes
The P8-5 scaffold built six library classes before any endpoint code existed: a database wrapper, a rate limiter keyed by IP, a token generator, a mailer, an email validator, and a config loader. Each class has one job. None compose into a micro-framework. The endpoints pull in what they need via require_once at the top of the file. The dependency graph is visible at a glance; read the require list and you know the surface area.
That is the tradeoff against a proper autoloader. PSR-4 with Composer gives you namespace resolution and lazy loading. It also requires a working CLI on the host, a vendor/ directory that is either committed or rebuilt on every deploy, and toolchain overhead for a backend with six classes and three endpoints.
Six classes with explicit requires is the right shape for this scope.
The schema first
P8-4 came before any PHP. cpanel-backend/schema.sql is 190 lines of DDL. The comment density is high, and the schema doubles as design documentation. Applied to captainrandomco_newsletter with scoped privileges, the application credential can INSERT, UPDATE, and SELECT. Not DROP.
If that credential is compromised, the blast radius is bounded: the list can be read but cannot be destroyed. DDL operations run as a separate admin credential, manually. This is not a sophisticated access model; it is a two-line privilege split that closes the most common accidental-deletion path.
The schema exists before the endpoints because it forces the data model to be explicit before the pipeline is built. Every database-touching stage in subscribe.php operates on columns that were reviewed before the code was written.
The subscribe pipeline
Eight stages. Each is a gate. Failure returns before the next stage runs.
Method check. POST only. Anything else gets a 405.
Origin check. The request’s Origin header must match a configured allow-list. Server-side defence-in-depth alongside browser-enforced CORS. An unrecognised Origin is rejected before any rate-limit logic runs.
Per-IP rate limit. Submissions above a threshold in a rolling window return a 429.
Honeypot and time-trap. A hidden field that real users never fill; a minimum-time check that real users naturally satisfy. Both are silent — the response is indistinguishable from a legitimate rejection on any other gate.
Email validation. Format check only, no MX lookup and no third-party service. Delivery handles existence at the network layer.
DB upsert. INSERT or UPDATE depending on prior state. The confirmation token is generated here and stored.
Confirmation mail. Double opt-in link sent. Fail-open: if delivery fails, the subscriber row persists in a pending state and a subsequent re-subscribe re-triggers the send.
Enumeration-resistant response. A fixed message regardless of upsert outcome. New address, already pending, already confirmed: the caller cannot distinguish them.
Enumeration is a low-effort probe. A bot submits addresses from a harvested list and reads whether the response differs between a subscribed and a new address. If it does, the attacker now knows which addresses are on the list, which is a GDPR-relevant data exposure for a UK-registered newsletter. The fixed response closes that read at zero cost to the user experience. Both messages are honest. The user who subscribed will receive the confirmation mail. The user who re-submitted an existing address does not need to know why they will not.
Standard checklist, not a novel design. None of it required a framework. Each stage is 10 to 30 lines of PHP. Together they fit in 225 lines.
The confirm and unsubscribe pattern
Both confirm.php and unsubscribe.php follow the same shape: GET renders an interstitial; POST performs the state change.
The motivation is specific. A GET that accepted a token and acted on it immediately would drop the token in three places: browser history, the Referer header on any subsequent navigation, and the server access log. On a shared host where log access is not tightly controlled, all three are real exfiltration vectors.
The GET interstitial breaks all three. The token arrives in the URL, the page renders a single button, and the button submits a POST. The token moves to the POST body: not the URL, not the Referer chain, not the access log. The implementation is three lines of routing logic and a one-button form.
The .htaccess and the runbook
Two files from P8-5 that are easy to skip and wrong to skip.
The .htaccess blocks direct HTTP access to the library directory. Without it, on Apache shared hosting, the class files are web-accessible. They should not be, because they are server-side includes rather than endpoints.
The ops runbook is the honest acknowledgement that this is not a managed environment. No zero-downtime deploy on cPanel. The runbook covers applying a schema change, rolling back a broken endpoint, and confirming that confirmation mail is routing correctly. Plain Markdown. The part of the 600 lines most likely to be written off as overhead, and most likely to matter at 11pm when something breaks.
The order
P8-4 through P8-7 shipped in four commits on the same day: schema first, then scaffold, then subscribe pipeline, then confirm and unsubscribe. The sequencing was deliberate. Data model before pipeline; infrastructure before endpoints.
The security properties are there because they were designed in. Enumeration resistance is a response pattern. The GET-then-POST token flow is a routing decision, and scoped privileges are two lines of SQL. None of it required Laravel.
The constraint (no Composer, no framework, a deployment target where the file manager is a legitimate ops tool) changed the implementation shape. It did not change what the implementation could achieve.



