Client Lockbox Membership documentation

WordPress plugin client-lockbox-membership 1.0.8 · Author 007AJ · Works with any WordPress theme

Install

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).

  1. In wp-admin go to Plugins > Add New Plugin > Upload Plugin.
  2. Choose client-lockbox-membership-1.0.8.zip and click Install Now.
  3. Click Activate Plugin. Activate it per site; multisite network activation is not supported.
  4. Open Client Lockbox in the wp-admin menu to configure levels, Stripe, portal, signals, downloads and starter sites.

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.

If a legacy Client Lockbox / Suite plugin is still active, deactivate it. Tables are not auto-deleted.

First-time setup guide

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.

Step 1 — Create a membership level

  1. Go to Client Lockbox → Levels.
  2. Under Add level, enter a Name (for example Gold Plan), a Price (0 = free) and an Interval (once, day, week, month or year). Optional: trial days, download cap, admin approval.
  3. Leave Level is available ticked and click Save level.

Step 2 — Find the level ID and the signup shortcode

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.

Step 3 — Create or assign the Signup, Account and Portal pages

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:

PageShortcodeShows
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

Step 4 — Connect Stripe (paid levels only)

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.

Step 5 — Test a signup

Open the Signup page in a private browser window, create a test account, then check Client Lockbox → Members and the Account page.

Administrators bypass membership. Users who can manage the site (administrators) can open the Portal page and all members-only content without an active membership — this is a staff/admin bypass so you can review everything while logged in. To see exactly what a member sees, use a separate non-admin test account.

Modules

Shortcodes and blocks

Output uses your active theme's styles.

Stripe

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.

Paid levels, plan changes and approvals

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.

Subscription cleanup and admin notices

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.

Updating from older versions: duplicate memberships

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.

The reconciliation queue and its retry schedule

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.

When the database cannot be written

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.

What each admin notice means

NoticeMeaningWhat 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 filesStorage warnings, see Private file storage.Add the Nginx rule shown, or set CLBM_STORAGE_DIR; check folder permissions.

Manual recovery

  1. Open the Stripe Dashboard → Billing → Subscriptions and search for the subscription id shown in the notice (or the customer email).
  2. If it is still active, choose Cancel subscription → Immediately. Issue a refund there if your policy offers one; the plugin never refunds.
  3. Back in WordPress, click I handled it in Stripe next to that line. The item is marked handled and leaves the notice.
  4. If you would rather let the plugin do it, fix the Stripe connection and click Retry now; it re-reads every queued subscription and cancels the live ones.

Deleting a user

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.

Levels, deactivation and uninstall

Private file storage

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).

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.

Files are moved automatically whenever the storage folder changes: when updating from 1.0.0, and when you add, change or remove 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.

Content restriction

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 search lookup table

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:

After updating: the backfill

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.

Starter sites

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).

External Services

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.

Privacy

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.

FAQ

Are files safe on Nginx?

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.

Can a signed link or product key be used more times than allowed?

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.

What happens if a Stripe webhook fails part-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.

Is a paid membership ever activated without payment?

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.

What happens to billing when I deny an approval?

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).

What happens to billing when I delete a user?

All of the user's subscriptions on this site are canceled, or queued if Stripe cannot be reached. See Deleting a user.

Can I delete a level that has members?

No. Untick Active to stop new signups instead.

Is multisite supported?

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.

Can a member end up with two active subscriptions?

No. See One membership per user under Stripe.

Do I need a product key?

No. All included features work when the plugin is active. There is no License tab or activation screen.

Changelog

1.0.8

1.0.7

1.0.6

1.0.5

1.0.4

1.0.3

1.0.2

1.0.1

1.0.0

Support

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.

© 007AJ · Home · Terms · Privacy · Support: admin@007aj.com