# How Multi-Target PostgreSQL Sync Manages Named Targets in agentsview

> Discover how agentsview's multi-target PostgreSQL sync uses named targets with scope identifiers to manage independent sync states and watermarks for multiple databases.

- Repository: [Kenn Software/agentsview](https://github.com/kenn-io/agentsview)
- Tags: deep-dive
- Published: 2026-07-04

---

**The multi-target PostgreSQL sync in agentsview isolates each destination's sync state by prefixing SQLite keys with a scope identifier derived from the `SyncStateTarget` name, enabling independent watermarks for multiple PostgreSQL databases.**

agentsview supports pushing session data from its local SQLite store to one or more PostgreSQL databases. The multi-target PostgreSQL sync implementation achieves this through a named target system that lives in [`internal/postgres/sync.go`](https://github.com/kenn-io/agentsview/blob/main/internal/postgres/sync.go), where each target maintains its own progress tracking without interference.

## Where the Named Target Logic Lives

The core implementation spans three files in the repository:

- **[`internal/postgres/sync.go`](https://github.com/kenn-io/agentsview/blob/main/internal/postgres/sync.go)** – Defines `SyncOptions`, the `Sync` struct, and the `pushSyncStateScope` helper that builds scope identifiers (lines 45–55, 191–221). Also implements `scopedSyncStateStore` (lines 93–121) which prefixes all sync-state keys with the target name.
- **[`internal/postgres/sync_test.go`](https://github.com/kenn-io/agentsview/blob/main/internal/postgres/sync_test.go)** – Validates that different targets keep independent watermarks and confirms legacy migration behavior.
- **[`cmd/agentsview/pg.go`](https://github.com/kenn-io/agentsview/blob/main/cmd/agentsview/pg.go)** – The CLI entry point where the `--target` flag maps directly to `SyncOptions.SyncStateTarget`.

## How Named Targets Become Scoped Keys

When instantiating a `Sync` via `New()`, the caller supplies a `SyncOptions.SyncStateTarget` string (e.g., `"prod"` or `"dev"`). The system converts this into a scope identifier:

```go
// Inside New() in internal/postgres/sync.go
syncStateScope := pushSyncStateScope(
    opts.SyncStateTarget,
    opts.Projects,
    opts.ExcludeProjects,
)

```

The `pushSyncStateScope` function (lines 191–221) builds the scope using these rules:

1. **If no project filters are applied**, the scope is simply the target name.
2. **If project include/exclude filters exist**, a deterministic fingerprint of the filter set is appended using the format `target:project-filter:<fingerprint>`.

This `syncStateScope` string is passed to `newScopedSyncStateStore`, which wraps the underlying SQLite `SyncStateStore`.

## ScopedSyncStateStore: Per-Target Isolation

The `scopedSyncStateStore` struct (lines 19–45) ensures complete isolation between targets by prefixing every key with the scope:

```go
func (s *scopedSyncStateStore) scopedKey(key string) string {
    if s.scope == "" {
        return key
    }
    return key + ":" + s.scope   // e.g., "last_push_at:prod"
}

```

This means logical keys like `last_push_at` and `last_push_target_fingerprint` are stored separately for each target. When `ReadLastPushAt` executes (lines 500–528), it computes the same scope via `pushSyncStateScope` and retrieves the appropriately-scoped value. Similarly, `Sync.Status` (lines 401–424) displays the watermark for the selected target, allowing the UI to show distinct "last push" timestamps per destination.

## Legacy State Migration

If a user previously ran `agentsview pg push` without the `--target` flag, sync state was stored under bare keys (e.g., `last_push_at`). When `SyncOptions.MigrateLegacySyncState` is true, the first push for a new target triggers the `ensureMigration` logic (lines 46–95):

1. Read existing legacy values from unscoped keys.
2. Copy them into the newly scoped key (`last_push_at:<target>`).
3. Clear the legacy keys to prevent reuse.

This ensures zero data loss when upgrading to the multi-target system.

## Practical Example: Pushing to Multiple Targets

The following example demonstrates pushing to distinct production and development PostgreSQL targets from the same local database:

```go
// Push to production target
prodSync, err := postgres.New(
    pgURLProd, "public", localDB,
    "my-machine", false,
    postgres.SyncOptions{
        SyncStateTarget: "prod",            // <-- named target
        Projects:        []string{"myapp"},
    })
if err != nil { log.Fatal(err) }
defer prodSync.Close()

if err := prodSync.Push(ctx, nil); err != nil { log.Fatal(err) }

// Push to development target (different name, same local DB)
devSync, _ := postgres.New(
    pgURLDev, "public", localDB,
    "my-machine", false,
    postgres.SyncOptions{
        SyncStateTarget: "dev",             // <-- another named target
    })
defer devSync.Close()
devSync.Push(ctx, nil)                    // uses its own watermark

```

Both instances share the same local SQLite database, but their watermarks are stored under distinct keys:

- `last_push_at:prod` – the production push watermark.
- `last_push_at:dev` – the development push watermark.

Each target reads its own watermark on subsequent runs, ensuring incremental sync without duplicate data.

## Summary

- **Named targets** are defined via `SyncOptions.SyncStateTarget` and transformed into scope identifiers by `pushSyncStateScope`.
- **`scopedSyncStateStore`** prefixes every sync-state key with the target scope, providing isolated watermarks per destination.
- **Legacy migration** automatically copies unscoped state to scoped keys when `MigrateLegacySyncState` is enabled, ensuring backward compatibility.
- The architecture allows a single agentsview instance to push to multiple PostgreSQL destinations while tracking progress independently for each.

## Frequently Asked Questions

### How does agentsview prevent sync state collisions between multiple PostgreSQL targets?

agentsview prevents collisions by using `scopedSyncStateStore` to prefix every sync-state key with the target name. When you specify `SyncStateTarget: "prod"`, the watermark is stored as `last_push_at:prod` rather than the global `last_push_at`, ensuring each target maintains its own independent progress tracking.

### What happens if I add a target name to an existing agentsview installation that previously used no target?

If `SyncOptions.MigrateLegacySyncState` is set to `true`, the system runs `ensureMigration` (lines 46–95) on the first push. This copies any existing unscoped values (like `last_push_at`) into the new scoped key (`last_push_at:<target>`) and clears the legacy keys, preventing data loss while transitioning to the multi-target system.

### Can I use the same local SQLite database with different project filters for different targets?

Yes. The `pushSyncStateScope` function incorporates a deterministic fingerprint of your project filters into the scope identifier. If you push `"prod"` with `Projects: []string{"myapp"}` and `"prod"` with `Projects: []string{"api"}`, agentsview treats these as distinct scopes (`prod:project-filter:<fingerprint>`), maintaining separate watermarks for each filtered view.

### Where does the `--target` CLI flag map to in the source code?

In [`cmd/agentsview/pg.go`](https://github.com/kenn-io/agentsview/blob/main/cmd/agentsview/pg.go), the `--target` flag value is passed directly to `postgres.SyncOptions.SyncStateTarget` when constructing the `Sync` instance. This field then flows through to `pushSyncStateScope` in [`internal/postgres/sync.go`](https://github.com/kenn-io/agentsview/blob/main/internal/postgres/sync.go) to generate the scoped key prefix.