web/imports/ isn’t enough.
fp pull downloads a fresh capture of the production site (posts, pages,
attachments, FSE templates) from a per-tenant S3 bucket into a
gitignored working dir, so fp apply can stage it into the local
stack just like a committed snapshot.
The capture itself happens in the prod cluster — the mu-plugin’s
SnapshotExporter component runs wp fp snapshot on a daily wp-cron
event at site-local midnight (and on-demand via the WP admin button at
Tools → Snapshot Export). fp pull only downloads what’s already up
there.What gets captured
The wire format is the existingfp.snapshot/v5 envelope used by
fp snapshot. The PII allowlist is the same:
| In | Out |
|---|---|
wp_posts (post / page / attachment) | wp_users |
| FSE templates, template parts, global styles, navigation | wp_comments |
Allowlisted wp_options (site title, page-refs, theme mods) | wp_usermeta |
Attachment refs in post_content | wp_user_meta |
wp_options outside the allowlist (transients, tokens) |
wp_posts rows only — no binary blobs in the
bundle. The browser fetches the actual assets directly from the prod
uploads bucket (the same one humanmade/s3-uploads serves the live
site from), so designer pages render with working images without any
GB-sized snapshot transfers.
One-time setup
1. Add the [pull] block to your site’s frankenpress.toml
<site>-production-snapshots-<region>-<account>. Check with whoever
manages your tg_frankenpress Terragrunt setup if you’re not sure.
2. Confirm your AWS credentials are wired
fp pull shells out to aws s3 — no AWS SDK, no fp-specific
credential discovery. Whatever you’d use to run aws s3 ls works:
sts:AssumeRole into the prod account’s Admin
role (the AllowAssumeAdminInProd policy in tg-security). Anything
that reaches that role gets read access to the snapshot bucket
automatically — no per-designer S3 policy edit needed.
Daily loop
Pull the latest snapshot
.fp/prod-snapshots/<slug>/, drops a .gitignore stub inside
.fp/prod-snapshots/ so the content can’t be accidentally committed.
List what’s available without downloading
Download a specific slug
Apply the pulled snapshot
fp apply looks in both web/imports/ (committed designer
captures) and .fp/prod-snapshots/ (pulled prod captures) when picking
the latest:
web/imports/ is committed
history — what the site is supposed to look like. .fp/prod-snapshots/
is ephemeral — a working corpus for theme dev, re-pulled when stale,
gitignored automatically.
See both sources in fp list
How prod is publishing the snapshots
You don’t have to touch the cluster side, but here’s what it does so the bucket isn’t a black box:Daily wp-cron event
The mu-plugin’s
SnapshotExporter component registers
frankenpress_snapshot_export to fire daily at site-local midnight
(wp_timezone()). The K8s wp-cron CronJob picks it up the next time
wp cron event run polls (within ~60s).On-demand admin button
The same hook also runs when an admin clicks Tools → Snapshot Export
→ Queue snapshot for next cron tick. The button schedules a one-shot
event on the same hook, so capture always runs in the wp-cron
CronJob’s WP-CLI context (where
wp export is available).In-process capture + upload
The component calls the existing
wp fp snapshot capture machinery
(no new pipeline) and uploads each output file to
s3://<bucket>/prod-<timestamp>/ via the AWS PHP SDK that
humanmade/s3-uploads already loads. No new container, no aws-cli
in the image.Author rewrite on apply
wp_users isn’t in the snapshot allowlist, so the captured author IDs
don’t resolve locally. The mu-plugin’s AuthorRemapper rewrites
post_author to your local admin (user ID 1) on every imported row
during fp apply. Without this, the WP Media Library and Posts list
render every imported entry as “(no author)”.
The rewrite is gated by FP_APPLY_REMAP_AUTHORS=1, which the apply
pipeline sets transiently for the duration of the wp import
subprocess. Normal site operations don’t see the env var, so the
filter is registered only during an in-flight apply.
Operator opt-in
A tenant’s prod chart needssnapshotExport.enabled=true for the
SnapshotExporter to wake up. See
components/charts for the values block. The
default is false, so non-opted-in tenants run identical chart code
with zero overhead.
service_user/prod/<site>-production-snapshots) via an
ExternalSecret CR in the GitOps repo. The IAM policy on the bucket
service user is scoped to s3:PutObject + s3:ListBucket only — no
read, no delete — so even if the pod is compromised the worst case is
junk in the bucket that the 7-day lifecycle reaps.
Out of scope (for v1)
- Auto-apply on pull.
fp pull && fp applyis two commands by design; the separation lets you inspect (fp diff) before applying. - Selective table / scope pull. Snapshot is the unit. If you need just the templates, pull the whole bundle and selectively apply.
- Multi-env. Prod-only. Staging environments today are designer dev-tier; staging-snapshot use cases haven’t surfaced.
fp prunefor.fp/prod-snapshots/. The 7-day server-side lifecycle does the work on the cluster side;rm -rf .fp/prod-snapshots/<slug>is the explicit local cleanup path.fp pruneoperates on the committed dir only.
Troubleshooting
`[pull].bucket is not set in frankenpress.toml`
`[pull].bucket is not set in frankenpress.toml`
The
[pull] block is required only when fp pull is invoked.
Sites that don’t pull leave it unset. Add it with the name of your
tenant’s snapshot bucket (e.g.
sts-production-snapshots-eu-west-2-533158516642).`no snapshots in s3://<bucket>/`
`no snapshots in s3://<bucket>/`
Two possibilities:
- The cluster-side
snapshotExport.enabledisn’t true on the tenant chart — check with whoever owns the GitOps repo. - The daily wp-cron hasn’t fired yet on a freshly-enabled tenant. Either wait until the next site-local midnight, or click Tools → Snapshot Export → Queue snapshot for next cron tick on the production site to seed the bucket immediately.
`aws s3 sync` exits non-zero
`aws s3 sync` exits non-zero
The error message includes aws’s own stderr. Common cases:
AccessDenied— your shell isn’t authenticated against the prod account. Wrap your command inaws-vault exec mkennedy --or checkaws sts get-caller-identity.NoSuchBucket— the[pull].bucketname is wrong or the bucket hasn’t been provisioned. Confirm with whoever owns the Terragrunt setup.InvalidAccessKeyId— your local AWS credentials are stale. Re-authenticate.
Slug collision between committed and pulled
Slug collision between committed and pulled
If a captured slug exists in both
web/imports/<slug>/ and
.fp/prod-snapshots/<slug>/, fp apply (and fp delete) hard-error
rather than silently picking one. The two dirs are meant to be
disjoint. Rename or remove the duplicate.Related
/designer-flow— the full local capture / apply / release loop/components/mu-plugin— theSnapshotExportercomponent and thewp fpWP-CLI surface/components/charts— thesnapshotExport.*chart values/operations/configuration— the full env-var reference