Every platform has one rule that, if you trace it, explains half the codebase. For MaidsMe it is this, no worker’s phone number appears publicly, anywhere, ever. The only phone on the public site is the company’s own, set by the admin. It sounds like a single checkbox of a decision. Implemented honestly, it touched the schema, every public query, the auto-fill, and how data sits on disk. This post is that rule, end to end.
Why the rule exists needs saying plainly. The workers on this platform are largely women in domestic work, and a public directory of their names, photos, and phone numbers is a harassment machine, whatever its intentions. Mediation is only real if contact details are actually gated, so the platform’s promise from post one lands, concretely, here. The rule’s implementation has four layers, and each exists because a shallower version leaks.
Layer one, queries never fetch what pages must not show. The public profile query selects named safe columns, and phone is simply not among them, so no template bug can display a value it never received:
// Public profile: the SELECT is the privacy boundary
$worker = $wpdb->get_row($wpdb->prepare(
'SELECT id, full_name, profession, experience_years, skills, summary, photo_id
FROM ' . $this->table_profiles . '
WHERE id = %d AND status = %s',
$profile_id, 'approved_visible'
));
Layer two, the Hire Now auto-fill respects the rule at the exact moment it is most tempting to break. The form pre-fills the worker’s name, profession, experience, skills, and summary as reference for the inquiry, and deliberately not the phone, the employer tells the admin who they want, and the admin makes the connection:
$reference = sprintf(
'Hiring inquiry for: %s | %s | %s years experience | %s',
$worker->full_name, $worker->profession,
$worker->experience_years, $worker->skills
); // phone intentionally absent
Layer three, storage. Phones exist in the database for the admin’s use, and they exist encrypted, so a leaked backup or a stray database dump is not a directory of numbers. Encrypt on write, decrypt only inside admin screens guarded by capability checks:
private function encrypt_phone($phone) {
return base64_encode(openssl_encrypt(
$phone, 'aes-256-cbc', $this->key(), OPENSSL_RAW_DATA, $this->iv()
));
}
// decrypt_phone() runs only inside current_user_can('manage_options') screens
Layer four, the one public phone. The company number renders from settings, get_option(‘mm_settings’)[‘company_phone’], the admin’s own line, shown site-wide, so employers always have a number to call, just never a worker’s. Four layers, one rule, and the deeper principle underneath, privacy enforced at the edge of display is decoration, privacy enforced at the query, the flow, and the storage is architecture.
A few things people ask me about this
Why filter at the query instead of hiding the field in the template? Because templates change, get copied, and get debugged by printing whole objects. A value the query never fetched cannot leak through any of that. The SELECT list is the enforceable boundary.
Is encrypting phone numbers in the database overkill for a small site? No, it is sized to the harm, not the site. A leak of this table is a harassment risk for vulnerable people, and openssl_encrypt on one column costs minutes to implement.
Next
With the privacy spine in place, the platform’s daily work is the hiring flow itself, the form, the inquiry queue, and giving everyone exactly what they need. That is the next post.
