---
title: "Discovery Mutation Sync"
description: "How Ring keeps opportunity and entity listings fresh after every change — cache, SSR, and realtime for founders and integrators"
locale: "en"
---
# Discovery Mutation Sync

> **Info**
> Filter this page with **Founder** / **Developer** in the docs sidebar. Founders learn *why* listings stay fresh; developers get modules, events, and extension points.

When someone creates, edits, or removes an **opportunity** or **entity** on a PostgreSQL-primary Ring clone, users expect the marketplace and directory to update **immediately** — without a nightly reindex job. Ring coordinates three lightweight steps on every mutation.

## The three-step freshness model

| Step | What users perceive | What happens under the hood |
|------|---------------------|----------------------------|
| **1. Cache bust** | List pages stop showing stale cards | `revalidateTag` clears role-scoped list caches |
| **2. Page refresh** | Detail and hub URLs show new data on next navigation | `revalidatePath` invalidates App Router SSR |
| **3. Realtime ping** | Open tabs update without manual reload | Tunnel publishes `opportunity:*` / `entity:*` events |

> **Tip**
> On Postgres-primary clones, **the database row is the search index**. There is no separate Elasticsearch table to rebuild — discovery queries read JSONB directly ([Data model](./data-model)).

### For founders

## Why founders should care

Your clone's **directory** (entities) and **marketplace of needs** (opportunities) are the core loop most rings monetize around. Stale listings erode trust faster than a missing feature.

### Typical scenarios

  
- **[Job board](/docs/features/opportunities.md)** — A member posts a contract — approved listings appear on `/opportunities` and subscribers get Tunnel notifications.

  
- **[Vendor directory](/docs/features/entities.md)** — A verified entity updates its showcase — profile pages and public directory refresh without ops intervention.

  
- **[AI matcher follow-up](/docs/features/ai-matcher.md)** — New opportunities trigger matcher pipelines; fresh cache tags ensure match cards reference current budget and deadline fields.

  
- **[Moderation workflow](/docs/features/admin.md)** — Admin approve/reject flows call the same sync — status changes behave like updates for subscribers watching the channel.

### Operator expectations

- **No manual reindex CLI** for standard CRUD — if lists look stale, check Tunnel transport and `DB_BACKEND_MODE` first.
- **Confidential tiers** share list cache tags; path revalidation is minimized for narrow audiences (see developer table below).
- **Maps / graph views** (Ringdom Maps) are a separate store today — entity CRUD does not auto-update map nodes.

### For developers

## Architecture

```mermaid
sequenceDiagram
    participant SA as Server Action / Service
    participant DB as DatabaseService (Postgres)
    participant Cache as invalidate*Cache
    participant RSC as revalidatePath
    participant T as Tunnel publishToChannel

    SA->>DB: create / update / delete
    DB-->>SA: success
    SA->>Cache: revalidateTag (role keys)
    SA->>RSC: hub + detail paths
    SA->>T: syncDiscovery(channel, id, event)
```

### Shared Tunnel helper

`lib/discovery/sync-discovery.ts` — realtime fan-out only (cache + paths live in domain wrappers):

{`export async function syncDiscovery(params: {
  channel: 'opportunities' | 'entities'
  resourceId: string
  event: 'created' | 'updated' | 'deleted' | 'status_changed'
}): Promise {
  const tunnelEvent = resolveDiscoveryTunnelEvent(params.channel, params.event)
  // status_changed → :updated on the wire
  await publishToChannel(params.channel, tunnelEvent, {
    id: params.resourceId,
    event: params.event,
  })
}`}

| Channel | Tunnel events |
|---------|---------------|
| `opportunities` | `opportunity:created`, `opportunity:updated`, `opportunity:deleted` |
| `entities` | `entity:created`, `entity:updated`, `entity:deleted` |

### Domain wrappers

| Resource | Module | Invoked from |
|----------|--------|--------------|
| Opportunities | `features/opportunities/lib/opportunity-mutation-sync.ts` | `create-opportunity`, `update-opportunity`, `delete-opportunity`, `auto-approval-service` |
| Entities | `features/entities/lib/entity-mutation-sync.ts` | `create-entity`, `update-entity`, `delete-entity`, moderation + KYC hooks |

### After any opportunity mutation

{`import { syncOpportunityDiscovery } from '@/features/opportunities/lib/opportunity-mutation-sync'

await syncOpportunityDiscovery({
  opportunityId: id,
  event: 'created', // | 'updated' | 'deleted' | 'status_changed'
})`}

### After any entity mutation

{`import { syncEntityDiscovery } from '@/features/entities/lib/entity-mutation-sync'

await syncEntityDiscovery({
  entityId: id,
  event: 'updated',
})`}

### Subscribe on the client

Wire topic listeners with **`useTunnelChannel`** — production hooks parse `syncDiscovery` payloads via `lib/discovery/parse-discovery-tunnel-message.ts` (`{ id, event }` + `message.event` like `entity:created`).

| Channel | Hook | UI wiring |
|---------|------|-----------|
| `opportunities` | `hooks/use-realtime-opportunities.ts` | `features/opportunities/components/opportunities.tsx` |
| `entities` | `hooks/use-realtime-entities.ts` | `features/entities/components/entities.tsx` — deletes splice locally; create/update soft-reloads the cursor feed |

See [Realtime transport](./real-time) and [Tunnel protocol](../features/tunnel-protocol).

### Revalidated paths

**Opportunities**

- `/[locale]/opportunities`
- `/[locale]/opportunities/[id]`
- `/[locale]/opportunities/my`
- `/opportunities` (on create / status change)

**Entities**

- `/[locale]/entities`
- `/[locale]/entities/[id]`
- `/[locale]/entities/my`
- `/entities` (on create / status change)

### Paths intentionally omitted

| Path | Reason |
|------|--------|
| `/[locale]/entities/add` | One-shot form; redirect after create |
| `/[locale]/entities/status/...` | Payment callbacks — unrelated to CRUD discovery |
| `/[locale]/confidential/entities` | Tag invalidation sufficient; avoids extra path churn |
| Ringdom Maps nodes | Separate graph store — not wired to entity list caches |

### Row mapping

Reads map `DatabaseService` rows through:

- `features/opportunities/lib/opportunity-db-mapper.ts`
- `features/entities/lib/entity-db-mapper.ts`

Legacy Firestore converters under `lib/converters/*-converter.ts` apply only when `DB_BACKEND_MODE=firebase-full`.

## Related documentation

  
- [features/tunnel-protocol](/docs/features/tunnel-protocol.md) — Prerequisite: channel subscribe SSOT and publishToChannel vs publishToUserTunnel.

  
- [architecture/real-time](/docs/architecture/real-time.md) — Deep-dive: TunnelHub broker and feature publish/subscribe matrix.

  
- [architecture/data-model](/docs/architecture/data-model.md) — Depends-on: JSONB collections synced by this pipeline.

  
- [api/opportunities](/docs/api/opportunities.md) — Same-workflow: REST surface + post-mutation hooks for opportunities.

  
- [api/entities](/docs/api/entities.md) — Same-workflow: REST surface + post-mutation hooks for entities.
