Skip to content
Patronage Ads Sync Docs
Esc
navigateopen⌘Jpreview
On this page

Ejection Path

How a Loop tenant resumes its Ads Sync connections single-tenant on the open-source core, with re-enrollment of credentials and state.

The Ejection Path is the informal promise that a Loop tenant can leave the managed offering and run the same syncs single-tenant on the open-source core. It holds because Loop and the Ads Sync Reference Deployment are two consumers of one seam: the same @patronage/ads-sync/run Qualified Run, the same controlSchemaSql control schema, the same Airbyte destination schemas, and the same connector images. Only the custody adapter and the scheduler differ.

This page describes the procedure. It is a runbook you execute, not a button, and it is not a guarantee of any timeline, price, or feature parity. What Loop adds on top of the seam (multi-connection scheduling, fan-out, approvals, run history, tenant policy) does not come with you. What comes with you is the data, the schema contract, and the ability to keep syncing.

The posture is informal on purpose. The glossary term is the stake: as long as the Reference Deployment and Loop invoke runs only through the seam, ejection stays a documented procedure instead of a migration project. If Loop ever needed a private seam API, the seam would be wrong.

what stays the same

concern Loop single-tenant on ejection
Run seam createQualifiedRunAdapter createQualifiedRunAdapter Same code, same invariants
Control schema ads_sync.* from controlSchemaSql ads_sync.* from controlSchemaSql Same tables
Destination schemas One isolated Airbyte Direct Load schema per Sync Connection Same Same tables and rows
Reporting views ads_sync_reporting.* from reportingViewSql Same Same queries keep working
Connector images Built from the shipped Dockerfiles Built from the shipped Dockerfiles Same digests
Committed state ads_sync.sync_state_commits per connection Same Re-enrolled as a seed

what changes

concern Loop single-tenant
Custody Per-tenant, version-pinned secrets in the Cloudflare secrets store; the client references connections, never secret names Your Worker secrets, read by workerSecretsCustodyAdapter; config names secrets
Postgres role Per-tenant, least-privilege schema-scoped writer and reader One role with database-level CREATE; Airbyte owns DDL
Scheduling Multi-connection fan-out One Cron trigger, one Sync Connection per Reference Deployment
Run trigger Loop tools and policy POST /runs with the Ads Sync Deployment Token, or the Cron trigger

procedure

Do the quickstart once against a scratch database first. Then follow these steps for the real move.

1. export from Loop

Ask Loop for the tenant’s Ads Sync export. It contains, per Sync Connection:

  • the connection definition (provider, connectionId, destinationSchema, selected streams, streamGroup, and the configured catalog JSON);
  • the latest committed state from ads_sync.sync_state_commits, as the Airbyte STATE JSON;
  • the committed watermark and the last successful window;
  • a pg_dump of the connection’s destination schema and of ads_sync_reporting, when you are not keeping the same Postgres.

Loop never exports credential values. Refresh tokens stay tenant-local under Loop custody and are not part of any export.

2. choose the database

You have two options:

  • Keep the same Postgres. Loop’s tenant schemas are standard Postgres schemas. Grant your new role database-level CREATE and read access to the existing destination schema. The Airbyte tables and the reporting views are already there.
  • Move to your own Postgres. Restore the dump, then apply the schema exactly as in the quickstart step 3. The statements are idempotent, so applying them over a restored dump is safe.

Either way, one database serves both the Worker read path (Hyperdrive) and the destination Container write path.

3. re-enroll credentials

Create the provider credential yourself: a new OAuth grant for Google Search Console or Google Ads, a new system-user token for Meta Ads. Put it in the provider’s source-config secret with wrangler secret put, as in the quickstart step 6. Read the Google Search Console caveats for the Testing-mode token limit.

Set POSTGRES_DESTINATION_CONFIG_JSON for your role and database, and ADS_SYNC_RUNNER_TOKEN for /runs. The secret names are the same names Loop’s custody adapter resolves internally; the values are yours alone from now on.

4. carry the watermark over

The Reference Deployment does not send Airbyte state to the connector. Every run it plans is windowed, and a windowed Qualified Run passes no state: the window’s start and end dates bound the read, and the append_dedup sync mode keeps the destination correct. You do not need the exported STATE JSON for the Reference Deployment.

What carries incremental continuity on ejection is the watermark. It is keyed by connection id alone and carries over as it is:

psql "$DATABASE_URL" -c "select connection_id, watermark_end from ads_sync.sync_watermarks"
psql "$DATABASE_URL" -c "select connection_id, stream_name, committed_at from ads_sync.sync_state_commits order by committed_at desc limit 5"

A custom consumer of @patronage/ads-sync that runs the seam without a window is different: there the custody adapter serves the provider state secret (for example GOOGLE_SEARCH_CONSOLE_SOURCE_STATE_JSON) as seedState, and the seam uses it when the control store has no committed state for the connection’s catalog and source-config hashes. Only that consumer shape needs the exported STATE JSON.

5. re-enroll the connection

Edit ads-sync.config.ts in the Reference Deployment to the exported definition: same connectionId, same destinationSchema, same selected streams and stream group, the exported catalog under config/. Keeping connectionId and destinationSchema identical is what makes the watermark, the state, and the Direct Load tables line up. The Reference Deployment runs one connection; for more, run more deployments.

6. deploy and verify

Deploy as in the quickstart step 5, then:

curl -s -X POST -H "Authorization: Bearer $ADS_SYNC_RUNNER_TOKEN" "$WORKER_URL/runs"
curl -s -H "Authorization: Bearer $ADS_SYNC_RUNNER_TOKEN" "$WORKER_URL/runs"

A correct re-enrollment produces one of two results: no_new_final_data when Loop’s last window already reached the current Final-Data Horizon, or a queued run whose windowStart equals the watermark you brought over. A run that starts from horizon - windowStepDays means the watermark did not carry; check connectionId before anything else.

Compare row counts in the destination schema and the reporting views before and after the first run. Then ask Loop to disable the tenant’s connection so two schedulers do not write the same schema.

what this is not

  • Not automatic. Every step above is yours to run.
  • Not a data-plane guarantee. Loop is not obliged to keep any particular export format stable across releases; the schema contract is the stable part.
  • Not multi-tenant. Ejection lands you in single-tenant custody (ADR 0047). If you need per-tenant custody again, that is Loop.

Was this page helpful?