WP Supabase Sync
A WordPress plugin that mirrors published content into Supabase Postgres, with diagnostics that name which permission layer actually refused you.
- published
- August 4, 2026
- read
- 4 min
- words
- 618
- stack
- 5
Overview
WordPress stays the source of truth and the editing experience. Supabase becomes the read layer, so a Next.js frontend or a mobile app can query content straight from Postgres instead of going through the WordPress REST API.
Plenty of plugins push data somewhere. This one is built around what happens when it breaks.
The problem it actually solves
Supabase has two independent permission layers, and they fail in ways that look identical from outside. The most common support conversation is someone debugging the wrong one.
SQLSTATE 42501 is the sharp example. It means two completely different things:
| What Postgres says | What's wrong | The fix |
|---|---|---|
permission denied for table wp_content | The GRANT is missing. Row level security was never consulted, so editing policies changes nothing. | grant select, insert, update, delete on public.wp_content to service_role; |
new row violates row-level security policy | The grant is fine. A policy's WITH CHECK refused the row. | Fix the policy, or write with the service role key. |
Hand someone the second answer when they have the first problem and you've sent them into the policy editor for an hour. wp supabase doctor tells them apart, and a test drops the grant and asserts the wording so the distinction can't rot.
The failing check also refuses to cascade. When a privilege check fails upstream, the checks that depend on it are greyed out as skipped rather than reported as failures, because a privilege failure makes them unanswerable. One accurate red line beats eight.
Design decisions
PostgREST over HTTPS, not a Postgres connection. PHP's request-per-process model exhausts a connection pool quickly on shared hosting, pdo_pgsql is frequently absent on managed WordPress hosts, and port 443 survives restrictive egress firewalls. The plugin holds no database connections.
The plugin never runs DDL. wp supabase schema --print emits migration SQL generated from your own settings, for you to review and apply. Giving a WordPress plugin authority to alter your schema, using a key that bypasses row level security, is more power than it needs.
Syncing is off until diagnostics pass. A plugin that refuses to write until it has verified its own configuration is the whole personality of the thing.
Unpublishing deletes the row. Content moving out of publish is removed rather than stored with status = 'draft'. Unpublished rows sitting in the table are one bad policy away from leaking.
Post meta is an allowlist, empty by default. Meta routinely holds page-builder blobs, licence keys and third-party plugin state. None of that belongs in a table a public frontend reads.
Reliability
Content changes go into a queue table rather than straight out over HTTP.
- Coalesced. A unique key on
(object_type, object_id)means a newer event replaces the pending one. Editing 100 posts fires several hundred hooks and produces exactly 100 queue rows. - Batched, one HTTP request per action per batch.
- Backoff of
2^attemptsminutes, capped at an hour. - Fails fast where retrying can't help. Only 5xx, 429 and timeouts retry. A 401 or a missing grant dead-letters immediately instead of burning eight attempts pretending it might fix itself.
- Crash recovery. A batch claimed but never finished is reclaimed after ten minutes.
One trap worth naming: WP-Cron only fires when someone visits the site. On a low-traffic install the queue can sit untouched for hours while everything looks healthy, so the cron diagnostic checks for exactly that.
Testing
Everything was run against a real local Supabase stack rather than asserted. 293 checks on both ends of the supported range, WordPress 6.4.3 / PHP 8.1.34 and WordPress 7.0.2 / PHP 8.3.33, with an empty error log on each.
The error translator is tested against recorded real responses, captured by deliberately provoking each error against a live stack. None of them is a string somebody imagined, which is why the 42501 disambiguation can be claimed honestly.