WordPress plugin client-lockbox-membership 1.0.8 · Author 007AJ · Works with any WordPress theme
This package is the Client Lockbox Membership plugin (client-lockbox-membership-1.0.8.zip). It works with any WordPress theme. Requirements: WordPress 6.3+ and PHP 7.4+. Upload the ZIP file as it is (do not unzip it).
client-lockbox-membership-1.0.8.zip and click Install Now.WP-CLI: wp plugin install client-lockbox-membership-1.0.8.zip --activate.
Updating: upload the new ZIP the same way and choose Replace current with uploaded. Member data and stored files are kept.
Do this once after activating the plugin. The same steps, with live ticks and buttons, are in Client Lockbox → Setup guide in your WordPress admin.
The level ID is created from the name when you save it (for example Gold Plan → goldplan). The Levels list shows it in the ID column, next to a ready-to-copy Signup shortcode. The name slug (gold-plan) is accepted too.
[clbm_signup] — no level needed. It uses the Default signup level from Settings if you set one; otherwise the only active level; otherwise visitors choose from all active levels on the form. If no level exists yet, visitors see “Signup is not available yet” and administrators see a link to create a level.[clbm_signup level="ID-or-slug"] — signup for one specific level, e.g. [clbm_signup level="goldplan"].Easiest: on Client Lockbox → Setup guide (or Settings) click Create pages for me. It creates the missing pages with the right shortcode and assigns them.
Or assign existing pages in Client Lockbox → Settings. An assigned page works automatically: if its content does not contain the shortcode, the plugin adds it when the page is shown. Settings shows the status of each page. The shortcodes are:
| Page | Shortcode | Shows |
|---|---|---|
| Signup | [clbm_signup] or [clbm_signup level="ID-or-slug"] | Account creation and joining a level (continues to Stripe for paid levels) |
| Account | [clbm_account] | Level, status, invoices, Stripe billing button; also the return page after Checkout |
| Portal | [clbm_portal] | The client file portal |
In Settings, paste your Stripe secret key (sk_test_… to test), add the webhook URL shown there in Stripe → Developers → Webhooks, and paste its signing secret (whsec_…). Free levels work without Stripe. See Stripe.
Open the Signup page in a private browser window, create a test account, then check Client Lockbox → Members and the Account page.
[clbm_portal]; files kept in the private storage folder (see Private file storage).[clbm_signals] / [clbm_signals_pnl]; optional Telegram/Discord. Signals are members-only: not in the REST API, search, sitemaps, feeds or archives; neutral addresses such as /signal/signal-123/.[clbm_downloads]; files kept in the private storage folder; per-level caps; optional buyer product keys. Only administrators can create Downloads and upload their files.[clbm_signup] or [clbm_signup level="ID-or-slug"] — signup form (see First-time setup); also available as the Membership signup block with a level dropdown.[clbm_checkout level=""] — checkout form for a level (optional coupon code) that continues to Stripe hosted Checkout.[clbm_account] — the signed-in member's level, status, invoices and a button that opens the Stripe Customer Portal.[clbm_portal] — client portal.[clbm_signals limit="50" status=""] and [clbm_signals_pnl limit="100"] — signal history and P&L table.[clbm_downloads] — protected downloads list.Output uses your active theme's styles.
Use Stripe hosted Checkout only. Save the secret key and webhook signing secret under Settings. Card numbers never touch your WordPress server. Prices, currency and trial length always come from the level saved on your site, never from the browser.
Webhook events to send (Stripe → Developers → Webhooks): checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.paid, invoice.payment_failed, charge.refunded, customer.deleted.
pending_approval without access. Approve grants access. Deny cancels the Stripe subscription immediately, so no further charges are made. Refunds of the first payment are not automatic: issue them in your Stripe Dashboard if your policy offers one. If Stripe cannot confirm the cancellation, the member is set to denied_pending_cancel (no access) and the subscription goes into the reconciliation queue (see Subscription cleanup).active and trialing have access; past_due, unpaid, canceled and expired do not. A refund of a subscription payment does not cancel the subscription; cancel it in the Stripe Dashboard if access should end. A full refund of a one-time purchase ends that membership; partial refunds are only recorded on the invoice. Disputes are not handled automatically.manage_options, edit_users, publish_posts, upload_files, unfiltered_html); such roles are refused.Webhooks only tell the site which subscription changed; the plugin then reads the subscription from Stripe and stores its status and dates. Events can therefore arrive in any order (for example customer.subscription.created before or after checkout.session.completed), and trial or period dates are never cleared by an incomplete event. Member records are updated field by field, so unrelated fields are never reset.
Webhooks are signature-checked, then each event is claimed as processing, handled, and marked processed only after it succeeded. If handling fails, the event is marked failed and the site answers with an error so Stripe retries it. A claim left by a crashed request expires after 5 minutes. Only successfully processed events are skipped as duplicates.
Whenever the plugin has to end a Stripe subscription (a duplicate membership merged on update, a WordPress user deleted, an approval denied, a second subscription replaced, or the plugin uninstalled with data deletion), it asks Stripe first. Any subscription Stripe does not report as canceled or incomplete_expired (for example active, trialing, past_due, unpaid, incomplete, paused) is canceled immediately; Stripe does not prorate or refund an immediate cancellation. A subscription that is already canceled is skipped. A subscription is never canceled twice.
Versions before 1.0.5 could leave a user with more than one membership row. On update the rows are merged so each user has exactly one. Before any row is removed, the plugin reads the status of every subscription on those rows from Stripe. It keeps the row whose subscription is active (if several are: the one active on your site, then the highest-priced level, then the newest), cancels the other live subscriptions and skips canceled ones. Every removed row is copied to an archive table first, and its invoices are moved to the kept row. The merge runs under a database lock and can safely run again (for example after an interrupted update). If Stripe is not connected yet during the update, nothing is canceled blindly: the extra subscriptions go into the reconciliation queue and are canceled once Stripe is connected.
A cancellation that cannot be done right away (Stripe not connected, an API error, a timeout) is stored in one reconciliation queue, together with the user, the Stripe customer, the reason and, for a deleted user, their email address and name. The queue is processed by the hourly WP-Cron event. The first retry is one hour after the item was queued; after that each failed attempt doubles the wait (1, 2, 4, 8, 16 hours), then once a day until it succeeds. Before each attempt the plugin reads the subscription from Stripe again, so a subscription you canceled yourself is simply marked done. An item leaves the queue only when Stripe confirms the subscription is canceled or you click I handled it in Stripe; it is never dropped. Items queued by version 1.0.6 or earlier are moved into this queue on update.
Every caller that ends a subscription (user deletion, duplicate merge, denial, a replaced subscription after a new Checkout, the webhook for a deleted user, uninstall and the hourly check) follows one rule: nothing is deleted or marked done unless Stripe confirmed the cancellation or the item was durably recorded. “Queued” means the queue row was written and read back. If the queue table cannot be written (a database error, a full disk), the item goes into a separate fallback store (the non-autoloaded option clbm_reconcile_fallback), with its own admin notice and one email; the hourly check moves it into the queue once writes work again. If neither can be written, the destructive step is skipped: when a user is deleted, the membership row, archived rows and pending Checkouts are kept and flagged, and the hourly check finishes the job later. A per-customer sweep (all of this site’s subscriptions for a deleted user’s Stripe customer) is closed only when every subscription was canceled, skipped or queued; otherwise it stays in the queue with backoff. Stripe lists are read page by page to the end; a failed page counts as a failure, never as a shorter list.
If WP-Cron is disabled on your server (DISABLE_WP_CRON), make sure a real cron job calls wp-cron.php, or run wp cron event run --due-now regularly; otherwise use Retry now.
| Notice | Meaning | What to do |
|---|---|---|
| Client Lockbox: Stripe subscriptions still need to be canceled. (with a list and Retry now) | One or more subscriptions are in the reconciliation queue. Each line shows the user (or “deleted user #ID” with the email address kept for you), the subscription id, the reason (duplicate membership merged, WordPress user deleted or removed from the site, approval denied, replaced by a newer purchase, plugin data deleted, membership record no longer exists, cancellation carried over from the earlier retry list) and the last error. The site email address gets one email when items start waiting (sent again only after the queue has been empty). | Connect or fix Stripe in Settings and click Retry now, or wait for the automatic retry. Or cancel the subscription in the Stripe Dashboard yourself and click I handled it in Stripe next to it. |
| Client Lockbox: Stripe items could not be saved to the reconciliation table. (with a list and Retry now) | A database write to the queue failed, so the listed subscriptions or Checkouts (with the user, or the email address of a deleted user) were saved in the fallback store instead. Local records of affected users are kept until they are recorded. The site email address gets one email. | Check the database (disk space, table permissions) and click Retry now, or wait for the hourly check. The items then move into the reconciliation queue. |
| Client Lockbox: the search access table could not be updated. (with Retry now) | A database write to the search lookup table failed more than once, or a failure is older than 15 minutes. The listed restricted posts are hidden from search for everyone except editors until the update succeeds; nothing is exposed. | Check the database (disk space, table permissions; the last database error is shown) and click Retry now. The update is also retried automatically every few minutes. |
| Client Lockbox Membership: Stripe is not configured, so these paid levels cannot be purchased… | Paid levels exist but no working Stripe connection. Paid signups are refused; nothing is activated without payment. | Add the Stripe secret key and webhook secret in Settings, or make those levels free or inactive. |
| Member denied, but Stripe did not confirm the subscription cancellation. | The member has no access (denied_pending_cancel); the subscription is in the reconciliation queue. | As for the queue notice above. The member becomes denied once the cancellation is confirmed. |
| The level was not deleted: N members use it or have a Stripe subscription for it. | A level is only deleted when no membership row uses it, including ended memberships that still reference a Stripe subscription. | Untick Active to stop new signups instead. |
| Client Lockbox: these deactivated coupons could not be deleted in Stripe yet… | The coupon is already refused on your site; the deletion in Stripe is retried. | Click Retry or wait for the hourly run. |
| private portal or download files are publicly reachable / could not move some stored files | Storage warnings, see Private file storage. | Add the Nginx rule shown, or set CLBM_STORAGE_DIR; check folder permissions. |
Deleting a WordPress user (Users → Delete, bulk delete, or removing a user from a site on multisite) ends every Stripe subscription that belongs to that user on this site, whatever the local membership status says. The plugin collects:
Each one is canceled if live, skipped if already canceled, or queued with the user's email and name if Stripe cannot be reached. The membership row is copied to the archive table before it is deleted; if even the queue cannot be written, the row is kept so no subscription id is lost. A Checkout paid after the user was deleted is canceled when its webhook arrives. Membership rows whose user disappeared without the normal delete (for example a direct database change, or a deletion on another network site while this plugin was not loaded) are found and handled by the hourly check. Invoices do not store a subscription id; their subscriptions are covered through the customer listing.
Portal and download files are never public media. They are delivered only through the plugin after a permission check (the signed-in owner or an administrator, or a valid signed link you created).
clbm-private-1a2b3c4d5e6f7a8b/) when that location is writable and not publicly served.wp-content/uploads/clbm-vault-…/) with .htaccess, web.config and index.php guards..htaccess), administrators see a persistent warning and a critical Site Health result with the exact rule to add. See Client Lockbox > Settings > Private file storage (with a Check again button).Nginx rule (use the folder name shown in your warning), then nginx -t && systemctl reload nginx:
location ^~ /wp-content/uploads/clbm-vault-xxxxxxxxxxxxxxxxxxxxxxxx/ {
deny all;
}
To choose the folder yourself, add define( 'CLBM_STORAGE_DIR', '/full/path/outside/web/root/clbm-files' ); to wp-config.php. CLBM_PORTAL_DIR and CLBM_DOWNLOADS_DIR still set each area separately.
CLBM_STORAGE_DIR (for example to move files out of a clbm-vault-… folder in uploads). Each file is copied, checked by SHA-256 and only then removed from the old folder; the move runs in batches on admin page loads and resumes where it stopped. Database records keep working because they store only file names. When the old folder is empty it is removed. A file that cannot be moved keeps being served from the old folder through PHP, and an admin notice lists the problem until the move succeeds.Tick Restrict this content in the Client Lockbox access box of a post or page and enter the required level ids (empty = any active member) and optional drip days. A restricted post is protected wherever WordPress can show it: the single view, archives and the home page, search results, RSS/Atom feeds, the REST API (posts, pages, search and comments endpoints), oEmbed embeds and comments. Visitors without access see the title and the members-only message instead of the text and excerpt; its comments are hidden and closed for them, and it is left out of their searches. The search check (front-end search, the search feed, REST /wp/v2/search and REST post/page lists with ?search=) runs inside the search query against a lookup table that the plugin keeps in step with each post's access settings. It has no size limit and costs the same number of queries however many restricted posts the site has. Administrators and anyone who can edit the post always see it. Signals follow the same rules and also hide their title.
The table {prefix}clbm_post_levels holds one row per restricted post and level (* = any active member) with its drip days. It is updated when you save a post, when the access settings change in any other way (for example by code or import) and when a post is deleted. Updates fail closed:
debug.log when WP_DEBUG_LOG is on), and a resync is scheduled with WP-Cron. If it keeps failing, the “search access table” notice appears with Retry now.When you update from 1.0.5 or earlier, existing restricted posts are added to the table in batches of 500 (on activation and then every minute through WP-Cron; about a second per 6,000 posts). Until a post is reached, it is hidden from search for everyone except editors; the single view, archives and REST access checks are not affected. If restricted posts still do not appear in members' searches some minutes after the update, WP-Cron is probably not running: run wp cron event run clbm_post_levels_backfill (each run works for up to 20 seconds and schedules the next one if posts are left), or set up a real cron job for wp-cron.php. Running the backfill again is harmless.
Open Client Lockbox > Starter sites to import one of five page sets: client portal, signals club, download club, course and community. Import one starter at a time. Pages are built from core WordPress blocks and take their look from your active theme. Rollback removes pages/menu created by that import and restores the previous front-page settings. Levels added by a starter are kept (additive).
By default, with no payment or chat credentials saved, this plugin does not call outside hosts. When an administrator saves Stripe credentials, the site contacts Stripe for hosted Checkout, the Stripe Customer Portal and inbound webhooks. Card details are entered on Stripe's pages and never reach your site. Telegram (api.telegram.org) and Discord (discord.com webhooks) are contacted only when Signals push is enabled with your own bot token or webhook. The storage self-test makes one HTTP request from your site to its own address (never to another host) to confirm private files cannot be downloaded directly. Nothing is sent to 007aj.com for product unlock checks. This plugin has no License tab and does not gate features behind a product key.
Client Lockbox Membership stores membership levels, member–level associations, order and invoice records (amounts, currency, status, payment-provider references — never card numbers), content-restriction and drip settings, email template settings, report aggregates, and (if you use those modules) portal file metadata, signal posts you create, download logs, and optional product keys you issue for your downloadable products, and records of Stripe subscriptions the plugin had to cancel (with the email address and name of a deleted user until that cancellation is resolved), in your site's database. Members can request an export or erasure through Tools → Export Personal Data / Erase Personal Data: the export covers the membership, rows archived when duplicates were merged, reconciliation records, invoices, download log, portal file list and portal activity, and it is exported in pages so every record is included; erasure removes the membership row (unless its Stripe subscription is still live or cannot be checked), archived merge rows (unless a subscription is still waiting in the reconciliation queue), the plugin's user meta and IP addresses in logs, and keeps invoices and portal files for your records. Portal and download file bytes are stored in a private folder outside the web root when the server allows it (otherwise in a randomly named folder inside uploads that the plugin tests for direct access) and are delivered only through the plugin after a permission check. The plugin sets no marketing cookies. It does not claim to make your site HIPAA- or GDPR-compliant; you remain responsible for your own policies, lawful basis and server security. When you delete the plugin, nothing is removed unless you tick "Delete all plugin data on uninstall" first. With it ticked, the plugin's tables, options, transients, scheduled events, Signals and Downloads posts, plugin post meta, plugin user meta (single site only; user meta is shared on multisite) and the private file folders are removed; live member subscriptions are canceled first, and any that cannot be are emailed to the site email address and kept in the reconciliation table. WordPress roles are never deleted.
Yes. By default they are stored outside the web root, so there is no URL to download them from. If your host only allows the uploads fallback, the self-test detects direct access and shows the exact Nginx rule to add until the files are blocked.
No. Each use is counted with one atomic database update that only succeeds while uses remain, so parallel requests cannot exceed the limit. Per-member download caps are enforced the same way.
The event is marked failed and the site returns an error, so Stripe retries it. It is treated as a duplicate only after it was processed successfully.
No. Paid levels are only activated after Stripe confirms the payment or subscription. Without a working Stripe connection, paid signups are refused with a clear message.
The Stripe subscription is canceled immediately. Refunds are up to you and are made in the Stripe Dashboard. If the cancellation fails, the member has no access and the subscription goes into the reconciliation queue (see Subscription cleanup).
All of the user's subscriptions on this site are canceled, or queued if Stripe cannot be reached. See Deleting a user.
No. Untick Active to stop new signups instead.
Activate the plugin on each site; network activation is blocked. Each site keeps its own levels, members and files. User accounts and user meta (Stripe customer reference, trial used) are shared across the network by WordPress.
No. See One membership per user under Stripe.
No. All included features work when the plugin is active. There is no License tab or activation screen.
/wp/v2/search and REST lists with ?search=. A batched backfill runs on update and hides posts from search until it reaches them.denied_pending_cancel with admin email, notice and hourly retry.[clbm_signup] without a level uses the default level, the only level, or a level chooser; level accepts an ID or slug; block level dropdown; Default signup level setting; Levels list shows ID and shortcode.finfo_close() calls.CLBM_STORAGE_DIR) now moves files from every earlier location, including the clbm-vault-… uploads folder, with verification, resumable batches, an admin notice on failure and removal of the empty old folder.Purchase includes 6 months of support. Use continues on 1 domain after support ends. Sold on WPBay and 007aj.com only. Contact admin@007aj.com.