N
HeadlessCoreHeadless Core
Back to Home

Theme Auto-Updates (self-hosted, license-gated)

Optional. Lets a customer who bought a license key get "Update Available" notices and one-click updates in wp-admin, the same UX as a WordPress.org theme — without actually being on WordPress.org (see Licensing for why this theme can't be listed there).

How it works

wp-admin (periodic core update check)
   → inc/updater.php: pre_set_site_transient_update_themes
      → GET {nextjs_url}/api/theme-updates/check?version=X&license=Y
         → verifies the license (src/lib/license.ts's verifyLicenseKey)
         → compares X against releases/manifest.json's "latest"
         → returns {newVersion, downloadUrl} or {newVersion: null}
      → injects the response into WP's own update_themes transient
   → customer clicks "Update Now" → WP downloads from downloadUrl
      → GET {nextjs_url}/api/theme-updates/download?version=X&license=Y
         → re-verifies the license, streams the versioned ZIP

No database, no third-party service (Freemius, EDD Software Licensing, etc.) — license verification is the same offline RSA-signature check used everywhere else in this project, just run server-side in Node instead of WordPress's PHP. releases/manifest.json is the only state, and it's a committed file, not a runtime database.

Cutting a release

node scripts/build-theme-release.js --version=1.0.1 --changelog="Fixed X, added Y"

This updates wp-content/themes/wp-headless-theme/style.css's Version: header, zips the theme into nextjs-starter-kit/releases/, and records it as the new latest in nextjs-starter-kit/releases/manifest.json. Commit both the style.css change and nextjs-starter-kit/releases/ together, then deploy — the next check request from any licensed customer site will see the new version.

Why releases/ lives inside nextjs-starter-kit/, not the repo root: Coolify's Nixpacks build only bundles the app's own base directory into the deployed container (confirmed from a real deploy log — the Docker build context was ~550KB, matching just nextjs-starter-kit/, not the whole repo). Anything the running app needs to read at runtime has to live where it's actually deployed. scripts/sync-template.js excludes releases/ from the npx create-headlesscore template, so scaffolded projects don't inherit old release ZIPs.

Setup

No env vars beyond what Stripe Licensing already needs: LICENSE_SIGNING_PRIVATE_KEY_PEM (used to derive the public key for verification — the update routes verify with the same keypair that mints keys). On the WordPress side, nothing to configure beyond what's already required for licensing to work at all — License Key and Next.js URL in Headless Core → Settings. If either is empty, inc/updater.php's hook is a no-op.

What this does NOT do

  • themes_api filter (the "View version X details" popup) — WordPress still shows the plain "Update Available" notice and Update button without it; only the optional details thickbox is missing.
  • Automatic (unattended) updates. This only makes the update visible and downloadable through WP's normal manual-update flow — it doesn't enable auto_update_theme behavior. Adding that is a one-line add_filter( 'auto_update_theme', '__return_true' ) if wanted later, deliberately not defaulted on here (an unattended update to code that hasn't been reviewed is a bigger blast radius than showing a notice).
  • Rollback. Only the latest version is ever offered; older ZIPs stay in releases/ for reference but there's no in-admin "downgrade" UI.
  • Revoke access on refund. Same limitation as Stripe Licensing — an expired/never-issued key fails the check, but there's no denylist for a specific revoked key that hasn't technically expired yet.