Scalability improvements
-
Hi
I am running a membership platform and I intend to have thousands/millions of users, so I used claude to review and audit your plugin and the response is stated below.
CLAUDE RESPONSE
I went through the full plugin — schema, models, WebAuthn crypto core, AJAX handlers, and background jobs. Overall it’s a solid, professionally-built plugin (proper prepared statements throughout, real WebAuthn clone-detection via signature counter, nonce + capability checks, rate limiting). But at “millions of users” scale, there are a few real problems, not just theoretical ones.
Security — mostly good, one real gap
- All queries use
$wpdb->prepare()correctly; the paginate/filter builder whitelists column names before interpolating them, which is the right pattern. - Nonce checks (
verify_nonce), capability checks (current_user_can), and login rate limiting are present on every AJAX endpoint. - Signature counter clone-detection is implemented correctly (
prevSignatureCnt >= signatureCounterthrows), which is the part plugins most often get wrong. - IP trust gap:
get_ip_address()defaultssecure_passkeys_trust_proxy_headerstofalse, so it readsREMOTE_ADDRonly. Since NigerPress sits behind Cloudflare,REMOTE_ADDRat your OpenLiteSpeed layer will be Cloudflare’s edge IP unless you’ve configured real-IP restoration (mod_cloudflare / trusted proxy headers in CyberPanel). If that’s not set up, every visitor shares the same “IP” from this plugin’s point of view — meaning the rate limiter effectively rate-limits all users together, and one abusive user can lock out logins for everyone behind that edge node. Worth checking your OpenLiteSpeed real-IP config, then adding a filter to trustX-Forwarded-For/CF-Connecting-IP(the plugin already supports this via thesecure_passkeys_trust_proxy_headersfilter — you just need to enable it and point it at the right header for your CF setup). credential_idis storedVARCHAR(255)after base64-encoding the raw binary ID. Most platform authenticators (Touch ID, Windows Hello, Android) produce short IDs, but some FIDO2 security keys (YubiKeys with resident/discoverable credentials) can produce IDs long enough that base64 pushes past 255 chars. On non-strict MySQL this truncates silently; on strict mode it throws. It’s an edge case, but at millions-of-users scale it’s a matter of when, not if, someone hits it. Bumping the column toVARCHAR(400)orTEXTwith a prefix index would remove the risk entirely.
Scalability — this is where the real concerns are
- Unbounded challenge table growth by default. Every login/registration attempt inserts a row into
secure_passkeys_challenges, but the cleanup cron only runsif ($challenge_cleanup_days > 0)— and the default setting is0(disabled). At scale, this table grows forever unless an admin manually opts into cleanup. This is the single biggest scale risk in the plugin. I’d recommend forcing a sane default (e.g. 30 days) rather than off. - Cleanup query itself doesn’t scale.
delete_old_records()usesDATEDIFF(NOW(), created_at) >= %d— wrapping the column in a function makes it non-sargable, so MySQL can’t use an index even if one existed (and neithercreated_atcolumn has one). On a multi-million-row table this becomes a full table scan holding row locks for a long single transaction — risky for replication lag and for anything else hitting that table concurrently. - No batching/chunking on delete. It’s one unbounded
DELETE FROM ... WHERE ...rather than deleting in batches (e.g.LIMIT 1000loops). On huge tables this can cause long-running locks and InnoDB undo-log bloat. - Plain wp-cron, not Action Scheduler. This is inconsistent with how you’ve built out your other plugins — wp-cron only fires on front-end traffic and has no locking/retry semantics, so on a high-traffic multi-tier setup the cleanup job can be skipped, double-run, or drift. Given you already use Action Scheduler elsewhere (Woocommerce), it’d be a natural, low-effort swap here too.
- Uncached admin dashboard aggregates. The overview screen runs
COUNT(*)on all three tables plus aJOINfor unique-user-count plus aGROUP BY aaguid(unindexed column) across the whole webauthns table, on every page load, with no transient/object-cache layer. At millions of credentials this becomes a genuinely slow admin page. Since you’re running Redis Object Cache Pro already, wrapping these in a short-lived transient (1–5 min) would cost nothing and fix it.
What already scales fine: per-user lookups (
user_idis indexed on all three tables), the credential-id unique constraint prevents duplicate-registration races at the DB layer even though there’s also an app-level check first, and rate limiting via transients will ride on your Redis object cache automatically.Really hope this suggestions can be implemented especially with the use of action scheduler, Note that action scheduler is already bundled with woocommerce.
Thanks
- All queries use
You must be logged in to reply to this topic.