Skip to content

External Slots ​

Wallaby provisions one publication and one logical replication slot for its own capture. If the same database also feeds a separate CDC consumer (an ELT or replication tool such as Airbyte, Debezium, or Fivetran running in pgoutput mode), that consumer needs its own publication and slot, scoped to its own tables.

AddExternalSlot has Wallaby create and maintain those for you, so the slot is provisioned and kept in sync as part of your normal deployment instead of being managed separately. Wallaby never consumes from an external slot; it only manages it.

Install (Optional) ​

External slots are part of the core package. If you only want Wallaby to provision slots for other tools, with no sinks of its own, the core package is all you need:

bash
dotnet add package Wallaby

With no sinks registered, Wallaby runs provision-only: it creates and reconciles the external slots, but never opens a slot of its own or streams changes. ForEntity<T>() and ForAllEntities() resolve against a storage provider's model, so they still need a provider (EF Core or Marten) registered; ForTable(...) works without one.

Declare a slot ​

csharp
builder.Services.AddWallaby(cdc =>
{
    cdc.UseEntityFrameworkCore<AppDbContext>()        // needed for ForEntity / ForAllEntities
       .UseConnectionString(conn)

       // Provision a publication + slot for an external ELT tool.
       .AddExternalSlot("elt", s => s
            .WithPublication("elt_pub")          // optional; defaults to "elt_pub" (= "{slot}_pub")
            .ForTable("public", "orders")        // by schema-qualified name
            .ForTable("customers")               // schema defaults to "public"
            .ForEntity<Product>());              // or by EF/Marten entity type (resolved to its table)
});

Point your external tool at the slot name (elt) and publication name (elt_pub) in its pgoutput configuration.

For a large model, include everything the registered providers map and carve out the exceptions:

csharp
cdc.AddExternalSlot("elt", s => s
    .ForAllEntities()                    // every table in the EF Core / Marten model
    .Except<AuditLog>()                  // by entity type
    .Except("public", "outbox")          // or by name (schema defaults to "public")
    .ForTable("legacy", "invoices"));    // explicit tables are added on top

ForAllEntities() is resolved at startup, so an entity added to the model joins the publication on the next start. It includes:

  • EF Core: every table the relational model maps, including owned types with their own table, TPT/TPC tables, and many-to-many join tables. A join table has no CLR type, so exclude it by name. Same-table and JSON-mapped owned types belong to their owner's table, so naming one with ForEntity/Except fails. A TPH hierarchy is a single table, so excluding one member excludes the whole table.
  • Marten: the table of every document type registered up front (StoreOptions.RegisterDocumentType or Schema.For<T>()). The event store is never included.
  • Keyless tables: tables without a primary key are skipped, because a table with no replica identity inside a publication makes the application's own UPDATE/DELETE statements fail. Only add such a table with ForTable if you've given it a replica identity.

Names are case-sensitive. Startup fails if an Except matches nothing, or names a table the slot also declares explicitly. Because every entity is included, a new entity whose migration hasn't run yet fails provisioning until the migration is applied.

External publications always publish whole tables. The PublicationColumnLists option only narrows Wallaby's own publication, never one consumed by a third-party tool.

API ​

MemberPurpose
AddExternalSlot(name, configure)Declare an external pgoutput slot named name.
WithPublication(name)Override the publication name (default "{slot}_pub").
ForTable(table) / ForTable(schema, table)Add a table by name (schema defaults to public).
ForEntity<T>()Add the table mapped to T, resolved against the EF Core or Marten model.
ForAllEntities()Add every table the registered providers model (see above).
Except<T>() / Except(table) / Except(schema, table)Remove a table from the ForAllEntities() set.

Each slot needs at least one table, since a pgoutput publication can't be empty. Slot and publication names must differ from Wallaby's own slot and publication, and from each other.

Lifecycle & semantics ​

  • Leader-only and idempotent: External slots are created in the same self-config step as Wallaby's own slot: on the leader, before streaming, every time a node becomes leader. Creating a missing slot or re-applying a publication is safe to repeat.

  • Reconciled: On each startup, Wallaby reconciles the external publication's tables to your declared list (ALTER PUBLICATION ... ADD/DROP TABLE). Wallaby owns that list, so a table added to the publication by hand is dropped on the next run. Manage membership through AddExternalSlot.

  • Pre-existing slots are adopted (and validated): If a slot with the declared name already exists, Wallaby reuses it instead of recreating it, and records it in wallaby.slot_registry. It fails fast if that slot isn't a pgoutput logical slot (e.g. a physical slot, or one using a different output plugin), so a name clash with an unrelated slot shows up as a clear startup error rather than a silent mismatch.

  • Never auto-dropped: Removing an AddExternalSlot(...) declaration does not drop the slot or publication. You have to remove a retired slot yourself:

    sql
    SELECT pg_drop_replication_slot('elt');
    DROP PUBLICATION elt_pub;

    The one exception is a suspension. Suspending Wallaby (e.g. for an RDS/Aurora major-version upgrade) drops every managed slot, external ones included, because the platform's precheck rejects any logical slot. The slot is recreated on resume, but the external consumer's position is lost and it has to re-sync.

  • Slot headroom: Wallaby's startup validation counts every slot it will create (its own plus all external ones) and fails fast with max_replication_slots guidance if there isn't room.

  • Bookkeeping: Each provisioned slot is recorded in wallaby.slot_registry. External slots are marked kind = 'external', and Wallaby's own slot is 'primary'.

WARNING

An external slot is created inactive and pins WAL from the moment it exists. If nothing consumes it, WAL keeps accumulating on the server. Only declare slots an external tool will actually read.

Scope ​

Wallaby only provisions external slots. It doesn't consume them, monitor their lag, or manage roles and grants for the external tool. Only pgoutput consumers are supported.

Released under the Apache 2.0 License.