WP Manifestindependent plugin directory
manifest / updates / zoer-connect

Zoer Connect

Zoer Connect WordPress transfers and verified recovery

★ 0stars
20release downloads
v0.5.3latest · 4d ago

Install

Download zoer-connect-0.5.3.zip From the v0.5.3 release on GitHub, Oct 9, 2026

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/ahzs645/zoer-connect/releases/download/v0.5.3/zoer-connect-0.5.3.zip
Updates itself. It ships its own updater, so new versions appear under Dashboard → Updates.

Zoer Connect transfers selected WordPress resources through HTTPS with verified recovery. Version 0.5.0 (API version 2) is qualified for stable publication, with real Zoer interface, public HTTPS, disposable DDEV and authorized Hostinger review-site evidence in the release notes. It moves toward WP Migrate DB Pro parity: filtered database Pull (tables, post types, revisions, spam, transients), theme/plugin selection modes and a media date filter, a read-only diagnostics preflight, opt-in import options (custom and variant replacements, path replacement, review before activation, late fencing, schema-validated table creation, author matching, activation-setting control, pause/resume, backup cleanup, destination-only find-and-replace, cache purging, safe errors) and batched Push uploads that pack many 256 KiB blocks into one request. Every new option is optional; clients that omit them get 0.3.14 behaviour. Earlier releases added resumable 2 GiB block publication (0.3.5), shared-hosting replacement and serialized-state preservation (0.3.4), and automatic Push setup on key generation (0.3.14). SparkLab was migrated with 0.3.x from its managed local source to https://sparklab.unbc.ca/; qualification evidence is recorded in the parent repository’s output/ui-audit/. WP Migrate was used as a workflow reference; this implementation is independent.

See the operating guide and implementation lessons for first-site setup, recovery, WP Migrate comparisons and remaining work.

Transfer workflow

The Zoer connection dialog supports remote Pull with pause/resume/cancel, and Push from a managed local DDEV source or from a verified Pull of another connected site. The local source exports its database in a background CLI worker on the existing DDEV server, using one InnoDB consistent snapshot. It does not depend on a hosting HTTP request staying open. Verified, immutable artifacts are downloaded to private Zoer storage and uploaded in authenticated 256 KiB blocks, one block per /chunks request or many per /batch request.

The destination imports through /wp-json/zoer-connect/v1/imports. Each request advances a durable private journal. SQL is parsed as data rather than executed. Destination tables are staged and verified before cutover. Selected files retain verified backups. A must-use bootstrap blocks ordinary WordPress requests during activation and serves authenticated recovery before regular plugins or themes execute. Finish reopens the site. Rollback checks every affected table and file before restoring anything; it tolerates regenerated transient caches and refuses substantive later edits.

Destination home/siteurl, administrator accounts, roles, connector identity, search-engine visibility and selected environment settings are retained. By default, source authors map to the destination connection's administrator (authorMapping: "match" maps by login, then email). Theme/plugin activation settings are preserved when their resources are not selected. After a database or plugin/theme file Push or rollback, the next normal WordPress request refreshes permalink rules without rewriting .htaccess. Every nonidentity source table must have a matching existing destination schema (or, with createTables, a strictly validated source schema); unsupported tables are rejected rather than silently omitted.

Destination setup

  1. From 0.3.14, generating a connection key enables Push and prepares Shared-hosting migration automatically. Pair that key with Zoer. Pull remains off until enabled. Older versions require enabling Push and shared-hosting migration manually.
  2. Automatic setup installs site-level MU protection and binds readiness to this site and protection code. It does not pause WordPress or publish content. Existing permissions, revoked access and advanced setup are preserved, including on key reset. If private storage or hosting compatibility prevents setup, the key remains available with an administrator error message; resolve the reported issue and save Push permissions to retry. Existing incomplete setups retain the manual recovery controls.
  3. In Zoer, test the connection, choose the source and resources, prepare an export, confirm the destination address and replacement, then import.
  4. Finish to reopen the site; inspect pages and administrator access. Use the same connection to recover interruptions. Rollback retains conflict checks and refuses detected later edits.

This is replacement, not a concurrent-edit merge. Earlier requests and external writers are not guaranteed to stop; avoid editing during migration. Requests reaching the site's MU protection pause during application/recovery. Original tables are retained by table renames and selected files have backups. Detected content changes can stop activation or prevent rollback. Coherent private journal storage and working filesystem/database locks remain necessary; distributed multi-host coordination is not provided.

Advanced verified-worker setup remains available for isolated environments that can prove all PHP workers adopted the fence. It retains Linux /proc inventory checks and writer declarations. Shared-hosting mode never records those declarations as established facts.

Changing fence code invalidates new-import readiness. Existing recovery remains available. Do not delete the connector, earliest MU bootstrap or private journals while a transfer is active. 0.5.0 changes the sealed fence runtime: repeat native request-protection setup before new source-pause exports or imports. Active transfers keep authenticated recovery; never update during an active transfer.

Supported boundary

  • Single-site WordPress 6.5+, PHP 8.1+, private writable storage outside every public document root.
  • Existing matching InnoDB tables with primary keys (or, with createTables, tables created from a validated source schema); foreign keys and triggers are rejected. Identity tables remain at the destination. Staged table names are limited to 48 characters.
  • Local exports: configurable quota (16 GiB by default, 1 MiB–64 GiB), 4 GiB per artifact, repeated 64 MiB free-space reserve checks, 30-minute worker deadline, immutable block checksums and restart/cancel handling. A failed worker starts a fresh transaction rather than appending an incomplete snapshot.
  • Destination SQL: at most 4 GiB; row batches at most 4 MiB. With an updated block-capable Zoer backend, individual selected files and existing destination originals support up to 4 GiB. Upload, verification, backup and restoration process 256 KiB blocks with durable progress. Older clients/jobs retain the legacy 32 MiB path. The configured incoming transfer quota and independent backup/restore free-space checks still apply. New import manifests are bounded to 8 MiB; other JSON import requests remain 2 MiB.
  • Upload requests: /chunks carries one 256 KiB block in at most 2 MiB of JSON; /batch carries up to 1,024 spans and by default at most 8 MiB (less when post_max_size is smaller), with about 2 seconds of server work per request.
  • Importable files: selected themes, plugins and media. Core, wp-config, MU plugins, connector files, executable uploads, symlinks and unsafe paths are excluded.
  • Advanced verified-worker setup requires readable Linux /proc process information and standard, single-threaded PHP-FPM, CGI or LSAPI workers. It checks every PHP process under the same operating-system account; a long-running CLI job or another site’s idle worker may delay readiness until it exits or adopts protection. Renamed/custom PHP interpreters, embedded or multithreaded runtimes, hidden process inventories and forked/unfenced background writers are unsupported. This is a compatibility check, not a guarantee for every shared host.
  • Shared-hosting mode in 0.3.9 permits regular advanced-cache.php and object-cache.php drop-ins. They execute before MU protection and can serve stale pages; exclude connector API routes from page caches and purge page/CDN caches after migration (purgeCaches calls known cache plugins but cannot purge a host or CDN cache). Database, sunrise and maintenance drop-ins, symlinked drop-ins and nonstandard content locations remain unsupported. Advanced verified-worker mode still rejects all early drop-ins. External SQL writers are outside the protection guarantee.
  • Hosted read-only Pull retains a consistent single-request database snapshot (256 MiB / up to 40 seconds). Optional source-pause export prepares larger snapshots across requests with typed keyset table/row checkpoints and checksummed SQL parts. It requires native key Push and Pull, request-protection readiness, InnoDB primary keys, MariaDB 10.1.1+ or MySQL 5.7.8+, and explicit confirmation that earlier requests and external/background writers stopped. HTTP paging cannot preserve one transaction across requests. SQL reads use a one-second database execution budget and two-second metadata lock waits; database interrupt checks are cooperative, so this is not a hard wall-clock deadline. Each part targets 4 MiB/about three seconds and permits a single SQL statement up to 16 MiB. Cancel or successful database sealing releases the source pause; idle expiry is one hour with a 24-hour maximum lifetime. Expired snapshots must start fresh.
  • Revoking/rotating a key invalidates its generation. Existing jobs cannot be rebound automatically to a new key. Keep the original authorized connection available for recovery. A WordPress administrator can still clean up finished imports of an earlier key from Tools → Zoer Connect.
  • Replacements: at most 50 custom rules and 250 rules in total; find/replace text up to 4,096 bytes, regex patterns up to 500 bytes. The per-value limit is 16 MiB (input and output) and applies only to values a rule can match; unrelated larger values pass unchanged (0.3.x: 1 MiB for every value). Literal replacements preserve unrelated serialized bytes without deserialization. Serialized objects or references requiring changes remain unsupported and are refused.
  • Matching terminal uploads can supply cached files under the same destination/key generation. Reused files are hashed before activation; database chunks are uploaded again. Cleaned-up imports cannot be reused.
  • Private backup journals are retained until cleanup. This is replacement, not merging concurrent edits or preserving arbitrary source user identities.

Authentication and API

Connection keys contain 256 random bits, are shown once, and are stored only as hashes in WordPress. Zoer stores its copy encrypted as a host-only secret. Requests use X-Zoer-Connection, never URL credentials. HTTPS, current administrator ownership and per-direction permission are checked. Public import/recovery uses native connector keys; application passwords remain supported for legacy staging endpoints.

  • /status: canonical destination, version, apiVersion, readiness, permissions, capabilities, batchTransports and batchLimits.
  • /diagnostics: read-only preflight inventory and warnings (Push or Pull permission).
  • /exports, /exports/paged: remote export create/status/step/chunks (or manifest/batch)/cancel.
  • /imports: idempotent create and list; /{id} status (?view=upload for the upload cursor); /{id}/chunks, /batch, /step, /finish, /rollback, /pause, /resume, /approve, /cleanup.
  • /jobs: legacy private file staging. Its old /publish route remains unimplemented; imports use the dedicated protocol.

Import requests are bounded to 2 MiB JSON (/batch octet-stream and multipart bodies are streamed and bounded by the batch limits); decoded upload blocks are 256 KiB. Job state, source/destination binding, sequence IDs and checksums determine retries. Clients never supply executable SQL or destination filesystem paths; sourcePath is only replacement text.

API version 2 (0.4.0)

All additions are optional. A client that sends none of them gets 0.3.14 behaviour, including identical idempotent export bindings, the 0.3.14 replacement rules and the early fence. Clients must check capabilities before sending an option. Everything below is exercised by the unit suites and tests/e2e-docker/; none of it is qualified on real hosting yet.

Status

GET /status adds apiVersion: 2, version: "0.4.0" and capability flags replacementRules, replacementVariants, reviewPause, createTables, authorMapping, keepActivePlugins, lateFence, importPauseResume, importCleanup, importList, siteReplace, cachePurge, databaseFilters, resourceModes, mediaSince, diagnostics and safeErrors (all true), plus batchUpload: true and batchDeflate (true when PHP's zlib inflate_init and inflate_add are both available). batchTransports lists octet-stream, multipart (only when PHP file_uploads is on) and json; batchLimits gives the octet-stream limits (see Batched upload).

Diagnostics

GET /diagnostics (connection key with Push or Pull permission) returns wordpress (version, prefix, home, siteurl, abspath, contentDir, uploadsDir, locale, permalinkStructure, blogPublic), php (version, memoryLimit, maxExecutionTime, postMaxSize, uploadMaxFilesize, zip/openssl/curl/mysqli), database (server mysql/mariadb, version, charset, collate, lowerCaseTableNames, up to 2,000 tables with suffix, prefixed, engine, rows, bytes, primaryKey, foreignKeys, triggers), postTypes with counts, themes (with active and parent), plugins (slug, file, active), muPlugins, dropins, warnings and pluginUpdate (current, and latest from WordPress's cached update check). Warning codes: firewall_plugin (Wordfence, Defender, Solid Security, All-In-One WP Security, Sucuri, NinjaFirewall or a firewall auto_prepend_file), page_cache_plugin (Breeze, WP Rocket, W3 Total Cache, WP Super Cache, LiteSpeed Cache, WP Fastest Cache, Cache Enabler, SG Optimizer, Autoptimize, or an advanced-cache.php drop-in), object_cache_dropin, mixed_case_tables, non_innodb, foreign_keys, triggers, blog_private, no_https. Nothing is changed. Paths are returned to authenticated clients only.

Pull filters and profile modes

POST /exports and /exports/paged accept database: true | false | {tables?, postTypes?, excludeRevisions?, excludeSpam?, excludeTransients?}. true keeps the 0.3.14 query byte for byte.

  • tables lists up to 500 suffixes after the prefix; only selected tables are read, so an unselected non-InnoDB table no longer blocks a Pull, and an unknown suffix is refused.
  • postTypes is an inclusion list (revisions are post type revision); excludeRevisions drops revisions. Postmeta, comments, commentmeta and term relationships are dropped only when their parent post (or comment) was excluded; link-category relationships and orphaned rows are kept.
  • excludeSpam drops comment_approved='spam' and its meta. excludeTransients defaults to true (the 0.3.14 behaviour); with a filter object the LIKE patterns escape _, so only real transient rows are skipped.
  • Filters are WHERE clauses inside the same consistent snapshot. Connector options, wpmdb* options, application passwords and sessions remain excluded from Pull. The snapshot's schema check ignores AUTO_INCREMENT, so a concurrent insert (a transient, a cron lock) no longer fails it with "Schema changed during export."
  • profile accepts themesMode/pluginsMode (all, active, selected, except), themesItems/pluginsItems (directory names, or a single-file plugin name such as hello.php) and mediaSince (YYYY-MM-DD: uploads modified on or after that UTC midnight). active includes the active theme and its parent. Absent or all values normalize to the 0.3.14 profile.
  • The response source adds abspath (untrailingslashed) and tables (exported suffixes).

Import options

POST /imports accepts an optional sourcePath (absolute source ABSPATH, at least two characters, no control characters) and an optional options object. Unknown keys are refused. Every key is optional:

Option Absent (protocol default) Zoer UI default Capability Effect
replacements.automatic true true replacementRules to turn off Source home and original URLs → destination URLs.
replacements.variants false true replacementVariants http/https twins, JSON-escaped (https:\/\/), rawurlencoded and protocol-relative (//host) forms.
replacements.paths false true replacementRules sourcePath → destination ABSPATH (and its JSON-escaped form), only when the source path has at least two segments and only up to a path boundary.
replacements.custom [] [] replacementRules Up to 50 {find, replace, regex?, caseSensitive?} rows, applied in order after the automatic rules. Regex rows need delimiters; caseSensitive: false adds the i modifier (regex) or matches literals case-insensitively.
replaceGuids true true replacementRules to turn off false leaves posts.guid unchanged.
keepActivePlugins null null keepActivePlugins null: keep the destination's active_plugins unless plugins are selected (0.3.14 inference); true: always keep; false: take the source's.
keepActiveTheme null null keepActivePlugins Same for template/stylesheet.
authorMapping administrator match authorMapping match adds the mapping_authors phase: source users are matched by user_login, then user_email; unmatched posts go to the connection administrator and unmatched comments are unlinked (user_id 0).
createTables false false createTables Missing destination tables are created, and mismatched schemas replaced (the old table kept as backup), from a strictly validated source schema.
partialDatabase false set by Zoer for table-subset Pulls databaseFilters or createTables Skips the check that the snapshot contains the nine core content/settings tables.
fence early (activation for kind: "replace") activation lateFence activation stages tables while the site stays live and pauses it only for file application and table swaps.
review false false reviewPause Stops at review_required until /approve. Requires fence: "activation"; refused otherwise.
purgeCaches false true cachePurge After finish or rollback, the next normal wp_loaded calls known page-cache purge hooks.

Absent options = 0.3.14 behaviour. When the create body has no options key at all, the import records legacyReplacements and applies the 0.3.14 rules byte for byte: every original URL, untrailingslashed, is replaced by the destination home, one literal rule after another in the given order (an earlier rule's output can be matched by a later one). With any options object (even {}), generated URL, variant and path rules run as one longest-match-first pass, so a destination URL that contains the source URL is never rewritten twice; original URLs beneath the source home (for example a WordPress subdirectory siteurl) map to the destination siteurl, and all others to the destination home. Generated rules match literally and case-sensitively; custom rows follow. Replacement counts per table and up to 20 before/after samples (240 characters around the first difference) are recorded.

Summaries of every /imports* response add kind, normalized options, fence, resumePhase, stats (replacements, per-table rows/replacements/created/schemaReplaced, samples), progress (uploadedBytes, totalBytes, tableIndex, tableCount, rowsRead), authors (matched, fallback), error, cleanedUp, updatedAt and finishedAt.

Phases

Early fence (default and 0.3.14; the site is paused from reserving on):

uploading → checking_artifacts → scanning_database → [mapping_authors] → reserving → preparing_tables → reading_database → verifying_tables → preparing_files → applying_files → activating_tables → verification_required → complete

Activation fence (the site stays live until the fenced check in reserving):

uploading → checking_artifacts → scanning_database → [mapping_authors] → preparing_tables → reading_database → verifying_tables → [review_required] → reserving → preparing_files → applying_files → activating_tables → verification_required → complete

In reserving, the activation fence first compares every live table with its staging fingerprint without pausing the site, then reserves the fence and checks again from scratch before any file changes. A live change detected at either point ends the import cancelled with a safe error, releases any fence and leaves the site live; nothing live was replaced. Live cron and maintenance options are re-copied into the options stage under the fence just before the swap. Imports without a database go from checking_artifacts to reserving (or review_required). reusing_artifacts precedes uploading when reuseImportId is set. /finish moves verification_required through finishing to complete.

Rollback before anything live changed (every phase before the fence; with the activation fence also preparing_tables through review_required, and reserving before its first check) simply ends the import cancelled; staged private tables remain until cleanup. Later rollbacks run rollback_reset → rollback_reset_files → rollback_preflight_tables → rollback_preflight_files → rollback_tables → rollback_files → rolled_back, refusing (with rollbackRefused, and the site reopened) when a preflight detects later edits.

Review, pause and cleanup

  • Review. At review_required the summary carries replacement stats, samples and author counts while the site is still live. POST /imports/{id}/approve moves it to reserving; repeating an approval after a lost response is a no-op. Rollback at this point discards the staged work (cancelled).
  • Pause/resume. POST /pause stores the current phase as resumePhase and sets paused; POST /resume restores it. Terminal phases, finishing and rollback phases cannot pause. Pausing does not release a fence that is already reserved: with the early fence, the site stays paused until the import is resumed and finished or rolled back.
  • Cleanup. POST /imports/{id}/cleanup accepts only complete, rolled_back or cancelled imports. It drops that import's zoer_b_, zoer_s_ and zoer_l_ tables in bounded (about 2 s) batches, reporting cleanedUp: false until the last one, then deletes artifact data, author maps, upload indexes and file backups. state.json remains with cleanedUp: true; rollback and upload reuse are refused afterwards. Cleanup is refused while another import is reusing the uploads.
  • Administrator cleanup. Tools → Zoer Connect → Transfers lists the newest 50 imports from private storage and offers Clean up backups for finished ones (administrator, nonce and confirmation required). It works on journals of any key generation, so backups from before a key rotation can still be removed; it remains bound to this destination and to terminal imports. The same page has editable export profiles with the theme/plugin modes and media date.
  • List. GET /imports returns {imports: [...]}, newest first, at most 50, for the current key generation and destination. Entries are light summaries (artifactCount instead of per-artifact arrays) and are read without the import lock.

Destination find-and-replace (kind: "replace")

POST /imports with kind: "replace", target, the migration-mode confirmation and options: {replacements: {custom: [...]}, tables?, review?, fence?, replaceGuids?, purgeCaches?} runs custom rules against this site's own database. At least one custom row is required; transfer fields (database, files, sourceUrl, sourcePrefix, sourcePath, originalUrls, resources, reuseImportId) are refused. tables lists up to 500 suffixes; the default is every prefixed table except users and usermeta, which can never be selected. The job starts in snapshotting: it stages and fingerprints each selected live table first, then writes a bounded snapshot of only those tables with DatabaseExporter (transients excluded; other tools' options such as wpmdb* settings are kept, unlike Pull), so any write after the fingerprint cancels activation instead of being overwritten. It then follows the normal pipeline with automatic rules, author/comment rewriting and table creation off and partialDatabase implied. fence defaults to activation; Zoer always sends review: true.

Table creation and other installations

A source CREATE TABLE is used only when it is a single statement of at most 64 KiB that starts with CREATE TABLE `<source name>` (, has exactly one column list closed by ENGINE=InnoDB and known table options, and has a PRIMARY KEY. It must not contain ;, any backslash, comments (--, /*, #), control characters other than newlines, an @, or (outside quoted identifiers and strings) FOREIGN KEY, REFERENCES, DATA DIRECTORY, INDEX DIRECTORY, CONNECTION, UNION, PARTITION, SELECT, LOAD_FILE, SLEEP, BENCHMARK, TABLESPACE, ENCRYPTION, generated/virtual/stored columns, INSERT_METHOD, TEMPORARY, AS ( or DEFAULT (. The table name is rewritten to the private stage name before execution. Created tables roll back by renaming them to a zoer_s_ name, which cleanup drops.

createTables and replace jobs refuse tables that belong to another WordPress installation sharing the database: a longer prefix that starts with this site's prefix and has its own options and posts tables (for example wp_blog2_ beside wp_).

Safe errors and cache purge

Import failures return {code: "zoer_import_failed", message, phase} (HTTP 400 for invalid input, 409 otherwise). The message is shown only when the plugin authored it (its own exception classes, or exactly RuntimeException/InvalidArgumentException); anything else becomes "Import could not advance. Retry or roll back using the same connection." Filesystem paths are replaced by [path] and the text is capped at 300 characters. The same object is stored as the import's error until the next successful step. Authentication and routing failures keep their zoer_import_unavailable responses.

purgeCaches queues the zoer_connect_cache_purge_pending option when the import finishes or rolls back. On the next normal wp_loaded the marker is deleted first, then LiteSpeed, WP Rocket, W3 Total Cache, WP Super Cache, Breeze, WP Fastest Cache, Cache Enabler, SG Optimizer and Autoptimize purge hooks and wp_cache_flush() are called, each isolated so a failing cache plugin cannot break the request or keep the site fenced.

Batched upload

POST /imports/{id}/batch uploads many 256 KiB blocks in one request. It is early-dispatched like every other import action, with identical key, Push permission and administrator-owner checks, and accepts uploads only in phase uploading. A batch is an ordered list of spans [artifactIndex, offset, length]: offset is block-aligned, length is a multiple of 256 KiB unless the span ends the artifact, and one request may carry many small files and several blocks of one file.

Transports (the client picks one per request):

  1. application/octet-stream (preferred): the body is "ZBT1", a big-endian uint32 header length H (2–65,536), H bytes of JSON header {"v":1,"spans":[[i,o,len],…],"payloadBytes":N,"enc":null}, then the span bytes concatenated. Content-Length is required and must equal 8 + H + payloadBytes. With "enc":"deflate" each span is [i,o,rawLen,encLen] and its bytes are a separate zlib stream (RFC 1950, not raw deflate): inflation stops at rawLen, and the stream must end exactly at encLen with exactly rawLen bytes, otherwise the span is rejected as decode. The body is read from php://input in pieces of at most 256 KiB (4 KiB for deflate input) and never buffered whole.
  2. multipart/form-data: a file part named batch carrying the same framed bytes, read from the uploaded temporary file. Advertised only when PHP file_uploads is on.
  3. JSON: {"v":1,"spans":[[i,o,len,"<base64>"],…]}, or with "enc":"deflate" [i,o,rawLen,encLen,"<base64 zlib>"], within the 2 MiB JSON limit.

Per block, the SHA-256 is verified against the manifest's chunkSha256 before anything is written (digest_mismatch). Then a block already inside the staged bytes is skipped when identical (idempotent retry) and rewritten when it differs; a block that overlaps the end is written at its offset (repairing a torn tail); a block beyond the end is refused as gap. Spans are processed in order and the request stops at the first rejected span (blocks before it stay applied) or between blocks once about 2 seconds have passed since the request started (deadlineHit; at least one block is always attempted). Artifacts without per-block digests (legacy whole-file manifests) are refused as unverifiable; clients upload those through /chunks. Out-of-range spans are bounds. Only import-<id>/artifact-N/data is written.

Response (small; no artifact list): {"v":1,"phase":"uploading","acceptedSpans","appendedBytes","cursor":{"index","offset"},"uploadedBytes","totalBytes","complete","deadlineHit","serverMs","rejected":null|{"span","code","expectedOffset"},"limits":{…}}. expectedOffset is the block-aligned staged size of the rejected span's artifact.

Limits: blockBytes 262,144; maxBatchBytes = min(post_max_size − 64 KiB, 8 MiB), and for multipart also at most upload_max_filesize − 64 KiB − 8 bytes (with post_max_size = 0 the PHP ceiling is 64 MiB); the zoer_connect_max_batch_bytes filter ($max, $transport) may lower or raise it within those PHP ceilings and 64 MiB, never below one block; maxJsonBatchBytes 2,097,152; maxSpans 1,024; deadlineMs 2,000. A body over these limits, or one PHP discarded because of post_max_size/upload_max_filesize, is answered with HTTP 413 {"code":"zoer_import_body_limit","message","limits"} carrying the limits for that transport, so a client can resize without guessing. An unread octet-stream tail is drained (bounded) before answering, so proxies deliver the response instead of resetting the connection.

Upload state is kept outside the journal so a batch never decodes every block digest: at create the plugin writes artifacts.idx (per artifact: uint64 bytes, uint32 first block, uint32 verified flag), blocks.idx (32 raw SHA-256 bytes per block) and, last, upload.json (cursor and byte counts), each through a flushed temporary file and rename. Journals created by earlier versions get them built once, lazily, while still uploading. Batch requests read the phase and binding from the journal's leading keys. GET /imports/{id}?view=upload returns {cursor, uploadedBytes, totalBytes, phase} without the import lock; the cursor offset is block-aligned, so a torn tail is resent.

Compatibility: /chunks responses and error messages are unchanged and share the same block writer, so a job may switch between /chunks and /batch mid-upload. The size check at uploading → checking_artifacts and the rehash in checking_artifacts remain the authority. Clients that do not see capabilities.batchUpload keep using /chunks.

Verification and packaging

Status (2026-10-03): 0.5.0 is qualified for stable publication. releases/0.5.0.json records exact runtime and deterministic package hashes; release notes record the integration evidence and bounds. Stable tags require this matching receipt and passing PHP/package CI. Development artifacts and prerelease tags do not update the public feed.

Run make test for PHP lint of every runtime file and the artifact-free suites (about 40 scripts, including disable_functions=link variants). CI runs them on PHP 8.1, 8.2 and 8.3. Without a local PHP, use the official images, for example docker run --rm -v "$PWD":/src -w /src php:8.1-cli make test and the same with php:8.3-cli. Connection form tests cover default Push, automatic shared setup, nonce/HTTPS/admin gates, setup failures, repeated submissions, permission preservation and revocation. They use a WordPress boundary fixture and do not replace real WordPress qualification.

tests/e2e-docker/ runs the full API version 2 lifecycle between two local Docker WordPress sites over HTTPS: status/diagnostics, filtered paged Pull, Push with every import option, review/approve, late-fence abort, rollback (refusal and success), cleanup, a legacy client, kind: "replace", pause/resume, safe errors and the batched /batch upload (MariaDB 11.4 by default, DB_IMAGE=mysql:8.0 for MySQL). See its README. The Zoer backend's backend/scripts/zoer-connect-batch-interop.ts drives the real Zoer Push uploader against the same fixture. Both are local substitutes, not hosting qualification. Real MariaDB/WordPress checks also live under tests/integration/; run these only against an explicitly authorized disposable destination. Integration evidence is recorded under the parent Zoer repository's output/ui-audit/.

After editing WriteFence.php or RequestDrain.php, run python3 scripts/seal-runtime.py before testing. Compiled source fingerprints prevent stale cached PHP code from certifying a new request-protection generation.

The source controller seals a whole-file SHA-256 and every block hash from the same verified bytes. The destination verifies each block against that authenticated manifest; it does not serialize a PHP SHA context across requests. Original files use recorded block digests for backup/conflict/restore verification. Existing legacy journals continue through their original recovery implementation.

Only after integration qualification, run make test-package and make build. tests/build_test.py builds twice to prove the ZIP is reproducible and asserts its exact file list: zoer-connect.php, readme.txt, LICENSE and every includes/*.php except the dev-only PeerImport.php (0.4.0 adds BatchUpload.php, CachePurge.php, Diagnostics.php, ImportError.php and TransferAdmin.php); update that list when adding a runtime file. Packaging is deterministic, with a SHA-256 sidecar in dist/. make build alone does not need a receipt, so it can produce a local 0.4.0 ZIP for disposable test sites; a local build is not a release, a live plugin update or website publication.

GitHub releases

Unqualified builds can be published for disposable-site testing with an annotated vVERSION-alpha.N, vVERSION-beta.N or vVERSION-rc.N tag. These run the PHP matrix and reproducible package checks, validate matching version metadata, and publish a clearly labelled GitHub prerelease with its ZIP and checksum. They require no qualification receipt, never update the public WordPress feed, and do not replace the latest stable release. Installing a prerelease ZIP does not establish hosting qualification; the checklist in releases/VERSION.md still applies before a stable tag.

The public source repository is ahzs645/zoer-connect. Zoer includes it as a pinned Git submodule at wordpress-plugins/zoer-connect; source changes are committed here before updating the parent pin. Branch pushes and pull requests run the PHP 8.1–8.3 suites. Stable release packaging additionally requires a version-specific qualification receipt in releases/, matching every packaged runtime file and the deterministic ZIP checksum. The package job runs on every push; branch and pull-request builds can produce unqualified development artifacts without a receipt, while stable tags always require one. Only v* tag runs publish anything. A runtime change therefore needs fresh integration qualification before stable publication succeeds; a receipt is a recorded maintainer assertion, not an automated integration test.

To release: update the plugin header, API/admin version and readme stable tag together; run the PHP and authorized disposable WordPress integration checks; build and verify the package; record its file hashes/checksum and results in releases/VERSION.json and releases/VERSION.md. Commit the scoped plugin changes and push an annotated vVERSION tag. The workflow rejects mismatched tags and publishes the ZIP plus checksum only after all checks pass. It also pushes the qualified ZIP and checksum manifest into the separate public ahzs645/zoer-connect-releases repository using a write deploy key scoped to that repository. Failed runs publish no new update feed; fix the failure before retrying. Do not move an already published release tag or change a published version's ZIP.

Install 0.3.12 or later once through WordPress's Upload Plugin screen. Its Update URI then checks the public latest.json feed during ordinary WordPress plugin update checks, and WordPress can install later qualified versions through Plugins → Updates. The plugin verifies the downloaded ZIP against the versioned release checksum before WordPress installs it. Automatic updates remain off unless a WordPress administrator enables them. The separate public update repository contains distributable ZIPs and checksum manifests. Source lives in ahzs645/zoer-connect; connection keys and site data are never included. Installing a plugin update does not push website content; never update the connector during an active transfer.

Storage troubleshooting (0.3.6)

Tools → Zoer Connect now reports a safe storage diagnostic code, the current path and PHP's filesystem restrictions (paths are visible to WordPress administrators only). Native status responses expose the code and a safe description. For first-time setup, an administrator can validate and configure a private folder outside the public roots without editing wp-config.php. The existing ZOER_CONNECT_STORAGE_DIR constant remains authoritative. The form refuses to move storage that contains transfers or has import protection installed; existing recovery data must remain at its original location. A host that does not permit any writable private folder still needs administrator configuration. No public uploads-folder fallback is used.

Paged exports (0.3.7)

Updated Zoer clients can negotiate pagedExport and use exports/paged. Traversal and immutable file preparation checkpoint after at most 250 entries or four seconds per request (one file copy/hash can extend that soft budget). Manifests return 500 entries per page. Downloads pack up to 32 chunks and 1 MiB into one response. Limits are 100,000 files, 4 GiB total, 32 MiB per source file and a separate database snapshot bounded to 256 MiB/40 seconds. Active requests renew the one-hour idle expiry within a 24-hour maximum lifetime. Earlier clients keep their existing export flow and limits. No full-site external clone is claimed by protocol qualification alone.

Exclude the connector REST namespace from any full-page hosting cache. Aram's Breeze cache required an explicit Never Cache URL entry even though connector responses set no-store. Confirm unauthenticated status returns 401 and a connection test reflects current plugin version and storage readiness.

Selective Push in Zoer 0.3.8

The matching Zoer interface can compare a prepared local export and select individual new/changed files by path. The plugin requires Push permission and checks destination fingerprints before publication. Database replacement is separately selected and does not merge records. Destination-only f

This README is longer than the copy stored here. Read the rest on GitHub →

Releases

13 releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
v0.5.3 latest
Oct 9, 2026 4d ago
update.json +2 assets
30
update.json27
zoer-connect-0.5.3.zip3
zoer-connect-0.5.3.zip.sha2560
Oct 7, 2026 6d ago
update.json +2 assets cohort
26
update.json22
zoer-connect-0.5.2.zip4
zoer-connect-0.5.2.zip.sha2560
Oct 3, 2026 10d ago
zoer-connect-0.5.1.zip +1 asset
3
zoer-connect-0.5.1.zip2
zoer-connect-0.5.1.zip.sha2561
Oct 3, 2026 10d ago
zoer-connect-0.5.0.zip +1 asset
0
zoer-connect-0.5.0.zip0
zoer-connect-0.5.0.zip.sha2560
Oct 3, 2026 10d ago
zoer-connect-0.4.0.zip +1 asset
1
zoer-connect-0.4.0.zip1
zoer-connect-0.4.0.zip.sha2560
Oct 2, 2026 11d ago
zoer-connect-0.4.0.zip +1 asset
1
zoer-connect-0.4.0.zip1
zoer-connect-0.4.0.zip.sha2560
Sep 29, 2026 14d ago
zoer-connect-0.3.14.zip +1 asset
1
zoer-connect-0.3.14.zip1
zoer-connect-0.3.14.zip.sha2560
Sep 23, 2026 20d ago
zoer-connect-0.3.13.zip +1 asset
1
zoer-connect-0.3.13.zip1
zoer-connect-0.3.13.zip.sha2560
Sep 23, 2026 20d ago
zoer-connect-0.3.11.zip.sha256 +1 asset
0
zoer-connect-0.3.11.zip.sha2560
zoer-connect-0.3.11.zip0
Sep 15, 2026 28d ago
zoer-connect-0.3.10.zip +1 asset
3
zoer-connect-0.3.10.zip3
zoer-connect-0.3.10.zip.sha2560
Sep 9, 2026 1mo ago
zoer-connect-0.3.7.zip +1 asset
1
zoer-connect-0.3.7.zip1
zoer-connect-0.3.7.zip.sha2560
Sep 9, 2026 1mo ago
zoer-connect-0.3.6.zip +1 asset
1
zoer-connect-0.3.6.zip1
zoer-connect-0.3.6.zip.sha2560
Sep 9, 2026 1mo ago
zoer-connect-0.3.5.zip +1 asset
3
zoer-connect-0.3.5.zip2
zoer-connect-0.3.5.zip.sha2561