EEditorialPack
System requirements & pre-deployment constraints

System requirements and pre-deployment constraints

These are the infrastructure rules for a secure cross-server WordPress REST API setup between an EditorialPack hub and its nodes. The page is written for the system administrator or WordPress architect doing the install. Work through every section on every site before connecting it. Most failed first connections trace back to a blocked REST namespace or a stripped Authorization header — both are covered below.

Production-validated. These constraints come from deployments proven on high-frequency, multi-site regional publisher networks in live production environments.


1. Technical prerequisites

1

HTTPS enforcement

Every hub and node serves over HTTPS with a valid certificate. Credentials travel in request headers and must never cross the network in clear text.

2

Application Passwords

Connections authenticate with WordPress core Application Passwords: one dedicated service user and one revocable password per connection.

3

Open REST namespaces

/wp-json/wp/v2/ and /wp-json/editorialpack/ must be reachable from sibling servers — through every WAF, CDN and security plugin.

Topology and baseline

  • One hub, many nodes. Each is a standalone WordPress installation. They may run on different servers, hosts and data centres.
  • Not Multisite-dependent. No shared database, filesystem or user table is required or assumed.
  • Network reachability. The hub must reach each node over HTTPS to deliver pings. Each node must reach the hub over HTTPS to pull events and download media. The hub does not need to be reachable by the public.
WordPress        5.6 or later      (Application Passwords are core from 5.6)
PHP              7.4 or later      (8.1+ recommended)
Transport        HTTPS only, valid certificate, TLS 1.2+
Permalinks       Any setting except "Plain"   (required for /wp-json/ routes)
Outbound HTTP    wp_remote_get / wp_remote_post permitted to sibling sites

HTTPS enforcement

HTTPS is mandatory on the hub and on every node. WordPress itself disables Application Passwords on non-HTTPS production sites.

// wp-config.php
define( 'FORCE_SSL_ADMIN', true );

Verify the certificate chain from the other server, not from your laptop:

curl -sSI https://node.example.com/wp-json/ | head -n 1
# Expect: HTTP/2 200

Warning. Self-signed or expired certificates cause silent pull failures. Do not disable sslverify to work around them. Fix the certificate.

WordPress Application Passwords authentication

No account password is ever stored on a remote site, and each connection can be revoked independently.

  1. Create a dedicated service user on the site. Do not reuse a human editor's account.
  2. Grant it only the role the connection needs.
  3. Under Users → Profile → Application Passwords, create one password per connection and name it after the remote site, e.g. editorialpack-hub.
  4. Enter the username and application password in the EditorialPack Network connection screen on the remote site.
  5. To cut a site off, revoke that one application password. Nothing else changes.

Confirm Application Passwords are available and the header survives your web server:

curl -sS -u "svc-editorialpack:abcd EFGH 1234 ijkl MNOP 5678" \
  https://node.example.com/wp-json/wp/v2/users/me | head -c 200
# Expect: JSON for the service user.
# If you get 401 "rest_not_logged_in", the Authorization header is being stripped.

Fix for a stripped Authorization header (common on Apache with PHP-FPM or CGI):

# .htaccess — above the WordPress block
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
# nginx + php-fpm
fastcgi_param HTTP_AUTHORIZATION $http_authorization;

Warning. Some security plugins disable Application Passwords globally or block the REST API for non-browser clients. Re-enable Application Passwords for the service user and add the exceptions below.

Unblocking the REST API namespaces

Hub–node traffic uses two namespaces. Both must be reachable from the sibling servers on every site:

/wp-json/wp/v2/            WordPress core — authentication, users/me, media
/wp-json/editorialpack/    EditorialPack Network — pings, event pulls, verification

Check them from the remote machine, not from a browser:

for ns in wp/v2 editorialpack/v1; do
  curl -sS -o /dev/null -w "$ns  %{http_code}\n" https://node.example.com/wp-json/$ns/
done
# 200 or 401  -> reachable (401 means auth is required, which is correct)
# 403         -> blocked by a WAF, security plugin or server rule
# 404         -> plugin inactive, or permalinks set to "Plain"

Add an allow rule wherever requests are filtered:

# nginx — do not rate-limit or challenge sibling servers on the plugin namespace
location ^~ /wp-json/editorialpack/ {
    allow 203.0.113.10;   # hub
    allow 203.0.113.20;   # sister nodes
    deny  all;
    try_files $uri $uri/ /index.php?$args;
}
# /wp-json/wp/v2/ stays publicly routable as usual — only exempt sibling IPs from
# rate limits and bot challenges there; do not "deny all" on it.
Cloudflare / WAF
  Rule:    URI Path starts with "/wp-json/wp/v2/" OR "/wp-json/editorialpack/"
           AND  IP Source in {sibling server IPs}
  Action:  Skip — Managed Rules, Bot Fight Mode, Rate Limiting, Browser Integrity Check
Security plugins (Wordfence, Solid Security, etc.)
  - Allow-list sibling server IPs
  - Do not set "REST API: restricted to logged-in users" without excepting both namespaces
  - Do not block non-browser User-Agents on these paths

Warning. A JavaScript challenge or CAPTCHA page returns HTTP 200 with HTML. A node cannot solve it. If pulls fail with a JSON parse error, a bot-protection layer is answering instead of WordPress.

Background processing

Nodes pull and apply events in the background. WordPress's default pseudo-cron only fires when a visitor loads a page, which makes delivery latency depend on traffic. On production, drive cron from the system scheduler.

// wp-config.php
define( 'DISABLE_WP_CRON', true );
# crontab — every minute
* * * * * cd /var/www/site && wp cron event run --due-now --quiet

2. Operational safe boundaries and content formatting guidelines

EditorialPack Network re-hosts media: each image is downloaded into the node's own media library, served from that node's domain and CDN, and every reference in the content is rewritten to the node's new attachment ID and URL. This is exact when the reference is structured, and impossible when it is an opaque number buried in a string. The boundaries below keep every reference structured.

Core blocks declare their media in machine-readable attributes:


<figure class="wp-block-image size-large"><img src="https://hub.example.com/wp-content/uploads/2026/03/vote.jpg" class="wp-image-4182"/></figure>

The node reads id: 4182, re-hosts the file, receives local ID 977, and rewrites the attribute, the class and the URL together. That is why native blocks are required for local CDN media library re-hosting:

  • Image and Gallery — the primary safe zone for all editorial imagery.
  • Cover, Media & Text, Video, Audio, File — handled the same way.
[gallery ids="4182,4183,4190"]

These numbers are hub attachment IDs. On a node they refer to different attachments or to nothing. The result is a gallery showing the wrong images, or an empty one — with no error, because the shortcode is syntactically valid.

Required action: convert legacy galleries to the core Gallery block on the hub before distribution. Select the Classic or Shortcode block and choose Convert to blocks. Audit existing content first:

wp db query "SELECT ID, post_title FROM wp_posts
             WHERE post_status='publish' AND post_content LIKE '%[gallery %ids=%';"

Out of bounds: Synced Patterns (formerly Reusable Blocks)


A Synced Pattern stores no content in the post. It stores a pointer to a wp_block post that exists only in the hub's database. On a node, ref: 3051 resolves to an unrelated post or to nothing, and the reader sees "Block has been deleted or is unavailable."

Required action: do not use Synced Patterns inside syndicated content. Select the pattern and choose Detach so its blocks are written into the post as ordinary core blocks. Unsynced patterns are safe: they insert real blocks at the time of use.

wp db query "SELECT ID, post_title FROM wp_posts
             WHERE post_status='publish' AND post_content LIKE '%<!-- wp:block {\"ref\"%';"

Further boundaries

  • Third-party blocks that store attachment IDs in custom attributes may not be remapped. Test each one on staging before allowing it in syndicated content.
  • Hard-coded hub URLs in Custom HTML blocks are not treated as internal links and will not pass through the link-healing sequence. Use normal editor links.
  • Page-builder content stored in post meta rather than post_content is outside the block parser. Author syndicated stories in the block editor.
  • Hot-linked external images are left as they are. Upload images to the hub's media library so they can be re-hosted.

3. Network infrastructure scale note

EditorialPack Network is optimized for routine daily publishing: a newsroom producing stories through the day, with each change appended to the event log and drained by the nodes as it happens. Size your expectations for bulk work accordingly.

  • Daily publishing is the design target. Individual publishes, updates and deletes, and ordinary editorial bulk edits, are what the event queue is tuned for.
  • Manual bulk operations run over a single PHP thread. A bulk action started from wp-admin executes inside one PHP request on the hub. Beyond roughly 50–100 historical posts in one operation, that request is exposed to max_execution_time and memory_limit like any other WordPress bulk action.
  • Keep manual bulk operations under that boundary. Select in batches of 50 or fewer and let the nodes drain the queue between batches.
  • Use CLI execution scripts for mass migrations. Back-filling an archive, onboarding a new node with years of history, or re-distributing thousands of posts is a migration, not a publishing task. Run it from WP-CLI, where there is no web request timeout, in controlled batches with pauses.

A migration script should follow this shape:

#!/usr/bin/env bash
# Batch a large historical distribution from the hub via WP-CLI.
# No web request is involved, so max_execution_time does not apply.
BATCH=50        # posts per batch — stay within the manual-operation boundary
PAUSE=30        # seconds between batches, so nodes can drain the event queue

wp post list --post_type=post --post_status=publish \
  --after="2019-01-01" --before="2024-01-01" \
  --orderby=date --order=ASC --format=ids \
| tr ' ' '\n' | xargs -n "$BATCH" | while read -r ids; do
    wp eval-file distribute-batch.php $ids    # your distribution routine for these IDs
    sleep "$PAUSE"
done
  • Run it off-peak, and watch each node's cursor in the delivery view. If a node falls far behind, lengthen the pause rather than the batch.
  • Media dominates migration time. Every historical image is downloaded and re-hosted by each node. Check node disk space before you start.
  • Ask us first. We will help you write the distribution routine for your targeting rules: hello@editorialpack.io.

Node resource guidance

The node does the heavy lifting: media downloads, image sub-size generation, link resolution. The hub's cost per story does not grow with the number of nodes.

memory_limit          256M minimum
max_execution_time    60+ for CLI/cron context
upload_max_filesize   >= the largest image your newsroom publishes
Disk                  Room for a full local copy of syndicated media

Pre-flight checklist

[ ] HTTPS valid on hub and all nodes, verified server-to-server
[ ] Permalinks not set to "Plain"
[ ] Dedicated service user + one Application Password per connection
[ ] Authorization header reaches PHP (users/me returns JSON)
[ ] /wp-json/wp/v2/ and /wp-json/editorialpack/ return 200 or 401 from each sibling server
[ ] WAF / CDN / security plugin exceptions in place for both namespaces
[ ] System cron driving wp-cron on every node
[ ] No legacy [gallery ids] shortcodes in content to be syndicated
[ ] No Synced Patterns in content to be syndicated
[ ] Third-party media blocks tested on staging
[ ] Historical back-fills planned as CLI migrations, not wp-admin bulk actions

Stuck on a step? Send the failing curl output to hello@editorialpack.io.