## How Apps and Webhook Modes Work

This document explains how **Apps** work in the CV API, with a focus on the `Client.webhooksURLMode` configuration:

- `**one_per_client`\*\*: one shared webhook URL per App (client)
- `**one_per_subscription`\*\*: one webhook URL (or action) per individual subscription

It is written for both **humans** and **AI agents** that need to reason about how to configure and use Apps correctly.

---

### Core Data Models

- `**Client`\*\* (`src/oauth2/schema/client.schema.ts`)
  - Represents an **App**.
  - Key fields:
    - `clientId`: unique identifier for the App.
    - `webhooksURL`: webhook endpoint owned by the App (when using `one_per_client`).
    - `webhooksAuthenticationHeader`: optional auth headers to send to the webhook.
    - `webhooksURLMode: WebhookUrlMode`:
      - `one_per_client`
      - `one_per_subscription`
    - `sendInvalidRefreshTokenWebhookNotification` / `sendWebhookWhenInvalidAccessOrRefreshToken`: flags that affect when webhooks are sent.
    - `owner_id`, `is_auto_register`, etc.
- `**UserApp`\*\* (`src/user-app/user-app.schema.ts`)
  - Represents the relationship **User ↔ App**, including subscriptions and webhook configuration.
  - Stored in collection `oauth2_user_clients`.
  - Key fields:
    - `user_id`: the CV user.
    - `client_id`: the App (`Client.clientId`).
    - `subscriptions: SubscriptionEventOrTrigger[]`: which events/triggers are enabled.
    - `webhookURL?: string`:
      - `null`/unused for `one_per_client`.
      - Required (or replaced by actions) for `one_per_subscription`.
    - `webhookAuthenticationHeader?: Record<string, any>`: per-subscription auth (used mainly with Zapier-style flows).
    - `subscription_actions?: SubscriptionAction[]`: actions for Trigger Action–style integrations.
    - `subscription_filters?: SubscriptionFilter[]`: optional filters.
    - `run_on_short_message: boolean`: Trigger Action–specific flag.
    - `scheduler_job_metadata`, `rrule`: scheduling metadata for time-based triggers.
- `**UserAppService`\*\* (`src/user-app/user-app.service.ts`)
  - Encapsulates business logic for subscribing/unsubscribing users and listing their apps.
  - Applies different rules depending on `client.webhooksURLMode`.
- `**UserAppController`\*\* (`src/user-app/user-app.controller.ts`)
  - HTTP API under `@Controller('apps')` with `@TokenAuth()`:
    - `GET /apps`
    - `POST /apps/:client_id/subscribe`
    - `DELETE /apps/:client_id/unsubscribe`
    - `DELETE /apps/:client_id/unsubscribe/:id` (Zapier / per-subscription use, excluded from Swagger)

---

## Webhook Delivery Modes (`webhooksURLMode`)

`webhooksURLMode` controls where the webhook URL lives and how many subscriptions map to a single `UserApp` record.

- `**one_per_client`\*\*
  - Webhook URL lives on the `Client` (`Client.webhooksURL`).
  - A user has a single `UserApp` record per app, containing many subscriptions.
- `**one_per_subscription`\*\*
  - Webhook URL lives on the `UserApp` (`UserApp.webhookURL`).
  - A user gets a new `UserApp` record for each subscription (effectively one subscription per `UserApp`).

The **same subscription endpoints** are used for both modes, but the **data model and responsibilities** change significantly.

### 1. `one_per_client` — One Webhook URL per App

**Definition (from `Client`):**

- `Client.webhooksURLMode === WebhookUrlMode.OnePerClient`
- `Client.webhooksURL` is the **single** webhook endpoint for all users and all subscriptions of this App.
- `UserApp.webhookURL` is typically **unused**.

**How subscription creation works**

- In `UserAppService.subscribe`:
  - When `webhooksURLMode !== WebhookUrlMode.OnePerSubscription`, the service calls:
    - `userAppRepository.upsertUserApp(payload)`
  - This **upserts** a single `UserApp` per `(user_id, client_id)` pair.
  - The webhook target is always taken from `**Client.webhooksURL`\*\*, not from the payload.

**Intended usage**

Use `one_per_client` when:

- Your integration exposes **one stable webhook endpoint** that:
  - Receives events for all of your users of that App, or
  - Uses its own internal routing / authentication to separate tenants.
- You do **not** need a different URL per subscription or per Zap-like entity.
- You prefer to manage:
  - Webhook URL configuration,
  - Authentication headers,
  - Rotation of secrets
    centrally at the **App (Client)** level.

**Pros**

- **Simple configuration**:
  - One webhook URL to configure per App.
  - No need to persist different URLs per user/subscription.
- **Centralized secret management**:
  - Rotate `webhooksAuthenticationHeader` or `webhooksURL` in one place.
- **Fewer records**:
  - One `UserApp` document per `(user, app)` instead of many per subscription.

**Cons**

- **Less granularity**:
  - Cannot point different subscriptions to different URLs.
  - Harder to “turn off” a single subscription endpoint; you manage that purely on your side.
- **Not a great fit for Zapier-like platforms**:
  - Those typically allocate one URL per Zap / recipe / subscription.

**Filters in `one_per_client` Apps**

- `subscription_filters` live on `UserApp`, not on `Client`.
- In this mode you typically have **one `UserApp` per user+client**, but that `UserApp` can still contain:
  - A set of `subscriptions` (events/triggers).
  - A set of `subscription_filters` that are applied **before sending** to the single `Client.webhooksURL`.
- This means:
  - You can scope which events for a given user are sent to the shared App webhook.
  - All events that pass the filters still go to the **same** URL; the filters are about **when** to send, not **where**.

**Required CV API endpoints for `one_per_client` Apps**

- **Subscribe user into App**
  - `POST /apps/:client_id/subscribe`
  - Input payload (simplified):
    - `subscriptions: SubscriptionEventOrTrigger[]`
    - (Plus optional filters / metadata as supported by `SubscribeUser`)
  - **Important**:
    - Do **not** send `webhookURL` in the payload for this mode; the platform uses `Client.webhooksURL`.

  **Example Request (webhook URL is NOT required):**

  ```json
  {
    "subscriptions": ["message.finished", "schedule"],
    "subscription_filters": [
      { "key": "channel_id", "value": "abc123", "operator": "equals" }
    ]
  }
  ```

  **Example Response (`SubscribedUser`):**

  ```json
  {
    "id": "66f0c8b2c0b1e00012345678",
    "client_id": "your-client-id",
    "user_id": "66f0c8b2c0b1e00099999999",
    "subscriptions": ["message.finished", "schedule"],
    "webhookURL": null,
    "subscription_filters": [
      { "key": "channel_id", "value": "abc123", "operator": "equals" }
    ]
  }
  ```

- **Unsubscribe user from App**
  - `DELETE /apps/:client_id/unsubscribe`
  - Removes the `UserApp` for that user and triggers logout via `Oauth2Service.logoutFromApp`.
- **List user’s Apps**
  - `GET /apps`
  - Returns `App[]`; if the caller is not `owner_id`, private fields like `webhooksURL`, `clientSecret` and `webhooksAuthenticationHeader` are stripped.

You **do not** normally use `DELETE /apps/:client_id/unsubscribe/:id` in this mode, because there is a single `UserApp` per `(user, client)` and subscriptions are not modeled as separate webhook URLs.

---

### 2. `one_per_subscription` — One Webhook URL per Subscription

**Definition (from `Client`):**

- `Client.webhooksURLMode === WebhookUrlMode.OnePerSubscription`
- Each subscription is backed by its own `UserApp` row, and:
  - `UserApp.webhookURL` (or `subscription_actions`) defines where/how to deliver events.

**How subscription creation works**

- In `UserAppService.subscribe`:
  - When `client.webhooksURLMode === WebhookUrlMode.OnePerSubscription`:
    - The service calls `validateSubscribeOneWebhookUrlPerSubscription({ payload, client })`.
    - Then it **inserts** via `userAppRepository.create(payload)` (not `upsertUserApp`).
- **Not upsert, not idempotent POST (duplicate identity):**
  - Each successful subscribe **creates a new row** when the payload’s identity is **new** for `(user_id, client_id)`.
  - If a row already exists with the **same** webhook URL, subscription events, filters, and actions (compared **order-insensitively**), the API returns **`400 Bad Request`** and **does not** update the existing document.
  - Repeating `POST /apps/:client_id/subscribe` (or `POST /apps/subscribe` for the default app) with the **exact same** identity is therefore **not** a no-op retry: treat it as an error unless you intend to detect duplicates client-side.
  - To **change** an existing subscription without creating a parallel row, use **`PUT /apps/:client_id/subscriptions/:id`** (subscriber-scoped), or create a new subscription and **`DELETE /apps/:client_id/unsubscribe/:id`** on the old id.

**Validation rules (`validateSubscribeOneWebhookUrlPerSubscription`)**

1. **One logical subscription per successful POST (one new `UserApp` row)**

- Each subscribe **inserts** a row when the identity is new. Duplicate identity (same webhook URL, **set** of subscription events, filters, and actions) returns **400** from the repository layer.
- `subscriptions` is an array of **event types / triggers** for that row. It **may contain multiple** values; the full set participates in duplicate detection (order-insensitive), same as on `PUT .../subscriptions/:id`.

2. **Special case: Trigger Action integration**

- For `client.clientId === 'cv-trigger-action-integration'`:
  - `payload.subscription_actions` **must be non-empty**.
  - `webhookURL` is **not required** for this special client.
  - This mode is used to store subscription **actions** (and related flags like `run_on_short_message`) instead of a plain webhook URL.

3. **All other Apps using `one_per_subscription`**

- `payload.webhookURL` **is required**.
- `payload.subscription_actions` **must NOT be provided**:
  - If present, a `BadRequestException` is thrown:
    - “Subscription Actions field is not allowed for this app”.

**Intended usage**

Use `one_per_subscription` when:

- You need **one URL or action per user subscription**, not per App:
  - Zapier-style integrations (one URL per Zap).
  - Systems where each recipe / rule / automation has a distinct HTTP endpoint or configuration.
- You need the ability to:
  - Add or remove **individual subscriptions** independently.
  - Attach different filters or actions to different subscriptions.

**Pros**

- **Maximum granularity**:
  - Each subscription can have its own:
    - `webhookURL`,
    - headers,
    - filters,
    - actions.
- **Per-subscription lifecycle**:
  - You can subscribe/unsubscribe **one Zap or rule at a time** without affecting others.
- **Better mapping to external automation platforms**:
  - Matches the way Zapier / similar tools think about webhooks.

**Cons**

- **More records**:
  - One `UserApp` per subscription; heavy users may create many records.
- **More client-side state**:
  - You must store and manage the `id` of each subscription (`UserApp._id`) if you want to delete it later.
- **Stricter payload requirements**:
  - One new subscription row per POST (no upsert); respect duplicate-identity rules.
  - Must respect the `webhookURL` vs `subscription_actions` rules.

**Filters in `one_per_subscription` Apps**

- Each subscription is its own `UserApp` row, and each row can have its own `subscription_filters`.
- Typical usage:
  - One `UserApp` per Zap / rule:
    - `subscriptions`: which event types that Zap receives (often one; can be several).
    - `subscription_filters`: conditions that must match before the event is sent to that subscription’s `webhookURL` (or actions).
- Practical effect:
  - Filters are **per-subscription** instead of per-user+client.
  - Different Zaps for the same user+client can listen to the same event type but with different filters and different URLs.

**Required CV API endpoints for `one_per_subscription` Apps**

- **Create a subscription**
  - `POST /apps/:client_id/subscribe`
  - Input payload (simplified, for “normal” external Apps):
    - `subscriptions`: **array of** `SubscriptionEventOrTrigger` values for this row (one or more).
    - `webhookURL`: **required**.
    - Optional `subscription_filters`, `webhookAuthenticationHeader`, etc.

  **Example Request (`webhookURL` required):**

  ```json
  {
    "subscriptions": ["message.finished", "message.posted.to.channel"],
    "webhookURL": "https://hooks.zapier.com/hooks/catch/123/abc",
    "subscription_filters": [
      { "key": "author_id", "value": "user_123", "operator": "equals" }
    ]
  }
  ```

  **Example Response (`SubscribedUser`):**

  ```json
  {
    "id": "66f0c8b2c0b1e00022222222",
    "client_id": "zapier-client-id",
    "user_id": "66f0c8b2c0b1e00099999999",
    "subscriptions": [
      "message.finished",
      "message.posted.to.message.posted.to.channel"
    ],
    "webhookURL": "https://hooks.zapier.com/hooks/catch/123/abc",
    "subscription_filters": [
      { "key": "author_id", "value": "user_123", "operator": "equals" }
    ]
  }
  ```

- **Unsubscribe an individual subscription (Zapier-style)**
  - `DELETE /apps/:client_id/unsubscribe/:id`
  - Parameters:
    - `client_id`: the App’s `clientId`.
    - `id`: the `UserApp._id` (subscription identifier).

  **Example Call:**
  `DELETE /apps/zapier-client-id/unsubscribe/66f0c8b2c0b1e00022222222`

- **Unsubscribe the user from all subscriptions for an App**
  - `DELETE /apps/:client_id/unsubscribe`
  - Removes **all** subscriptions (`UserApp` records) for that user+client.
- **List user’s Apps**
  - `GET /apps`
  - Returns logical `App` objects, not individual subscriptions.

---

## Choosing the Right Mode

### When to choose `one_per_client`

- You have:
  - A **stateless, multi-tenant webhook endpoint** that can handle events for all users.
  - Internal routing logic based on identifiers inside the payload.
- You want:
  - **Simpler configuration** for integrators.
  - **Centralized** management of webhook URL and auth.
- You do **not** need:
  - Per-subscription URLs.
  - Fine-grained unsubscribe logic at the “Zap” level.

**Short rule of thumb**:  
If a single webhook URL per App is enough and the external system can route internally, use `**one_per_client**`.

### When to choose `one_per_subscription`

- You integrate with:
  - Platforms that create a **distinct webhook URL per automation** (e.g. Zapier).
  - Systems where each rule / workflow has its own endpoint or independent lifecycle.
- You need:
  - **Per-subscription unsubscribe** and configuration.
  - Different filters / headers / actions for each subscription.

**Short rule of thumb**:  
If each subscription (Zap, rule, workflow) needs its own endpoint or configuration, use `**one_per_subscription**`.

---

## Endpoint Summary by Mode

### Common requirements (both modes)

- All `/apps` endpoints are protected with `@TokenAuth()` and require:
  - A valid user token (`@CurrentUser`).
  - For routes with `:client_id`, a resolved app consistent with auth (OAuth token for that client, allowlisted portal token + owned target, or PAT `cv:write` on an owned app—see product docs).
- Shared endpoints:
  - `GET /apps`
  - `POST /apps/:client_id/subscribe`
  - `DELETE /apps/:client_id/unsubscribe`

### Differences

- `**one_per_client**`
  - Use:
    - `POST /apps/:client_id/subscribe`
    - `DELETE /apps/:client_id/unsubscribe`
  - Do **not** depend on:
    - `UserApp.webhookURL` in the payload.
  - Webhook target lives at:
    - `Client.webhooksURL`.
- `**one_per_subscription**`
  - Use:
    - `POST /apps/subscribe` (optional): same body as subscribe, but the API provisions or reuses the user’s **default** portal app (`one_per_subscription` + `is_default_subscription_client`).
    - `POST /apps/:client_id/subscribe` (one subscription at a time).
    - `PUT /apps/:client_id/subscriptions/:id` to update an existing subscription **in place** (subscriber only).
    - `GET /apps/subscriptions` to list subscriptions you created for apps you own.
    - `DELETE /apps/:client_id/unsubscribe/:id` for **per-subscription** removal.
    - `DELETE /apps/:client_id/unsubscribe` to remove all subscriptions and log out.
  - Webhook target / action lives at:
    - `UserApp.webhookURL` or `UserApp.subscription_actions` (Trigger Action).

---

## Updating vs Upserting (How Changes Behave)

Understanding **how updates behave** is critical, especially for AI agents that generate integration code.

### `one_per_client`: subscribe is effectively an “upsert”

For Apps using **`one_per_client`**:

- `UserAppService.subscribe` calls:
  - `userAppRepository.upsertUserApp(payload)` when `webhooksURLMode !== OnePerSubscription`.
- This means:
  - There is **at most one** `UserApp` per `(user_id, client_id)`.
  - Calling `POST /apps/:client_id/subscribe` multiple times for the same user+App:
    - **Creates** the `UserApp` on first call.
    - **Updates/overwrites** it on subsequent calls (subscriptions, filters, headers, etc.).
- From a consumer’s point of view:
  - `POST /apps/:client_id/subscribe` is **idempotent** for a given user+App:
    - You can “upsert” the configuration by calling it again with the full desired state.
    - Think of it as “set my subscriptions/filters for this App to exactly this payload”.

**Guidance**

- To **change** subscriptions / filters / headers for a `one_per_client` App:
  - Build the new desired payload (full picture).
  - Call `POST /apps/:client_id/subscribe` again.
  - The previous `UserApp` row will be updated in place.

### `one_per_subscription`: subscribe inserts; duplicates are rejected (not upsert, not idempotent POST)

For Apps using **`one_per_subscription`**:

- `UserAppService.subscribe` calls:
  - `userAppRepository.create(payload)` (after validation)—**never** `upsertUserApp` for this mode.
- This means:
  - Each call **inserts** a new `UserApp` row when the subscription is **not** a duplicate.
  - A duplicate is defined as the **same** `webhookURL`, **same** subscription events, **same** filters, and **same** actions for that `(user_id, client_id)`, compared in an **order-insensitive** way (event list, filter list, and filter array values).
  - If the API detects a duplicate, it responds with **`400 Bad Request`** and does **not** create or update a row.
  - **`POST .../subscribe` is not idempotent** for identical payloads: same identity → **400**, not “update existing.”
  - A **changed** payload (e.g. different `subscriptions` only, same webhook) is treated as **non-duplicate** and **inserts another row**—so do not use repeated POST as a substitute for **PUT** when you mean “update this subscription.”
  - Persist the `id` returned in `SubscribedUser` for `DELETE .../unsubscribe/:id` and for **`PUT /apps/:client_id/subscriptions/:id`**.

There are two main patterns for “updating” in this mode:

1. **Update in place (preferred when you already have the subscription id)**
   - Call **`PUT /apps/:client_id/subscriptions/:id`** with the allowed fields (`SubscribeUser`-shaped payload subset handled by `UpdateSubscriptionPayload`).
   - Authorization: only the **subscriber** (`user_app.user_id`) may update that `id`; app ownership alone is not sufficient to edit another user’s subscription.

2. **Create new, then delete old**
   - For migrations or when you do not store the old id:
     1. `POST /apps/:client_id/subscribe` with the new configuration (must not exactly match an existing row’s identity, or you get **400**).
     2. Store the new subscription `id`.
     3. `DELETE /apps/:client_id/unsubscribe/:id` for the **old** subscription `id`.
   - Useful when mirroring external “new Zap, retire old Zap” flows.

**Guidance**

- For **`one_per_subscription`** Apps:
  - Do **not** document or assume “POST subscribe = upsert” or “repeat POST = safe retry”: duplicate identity returns **400**; near-duplicate creates **another** row.
  - Treat each successful **non-400** subscribe as **creating a new subscription**; persist `SubscribedUser.id` for updates and deletes.
  - Prefer **`PUT .../subscriptions/:id`** to change webhook URL, events, filters, or actions without growing row count.

In short:

- **`one_per_client`** → Think in terms of **one mutable config per user+App**, updated via **upsert**.
- **`one_per_subscription`** → Think in terms of **many subscriptions**, each with its own ID; **POST subscribe = insert** (duplicate identity → **400**); use **`PUT /apps/:client_id/subscriptions/:id`** or create+delete to change behavior.

---

## Guidance for AI Agents

When writing code or generating API calls:

- **Always read `Client.webhooksURLMode`** before deciding:
  - Whether to include `webhookURL` or `subscription_actions` in `POST /apps/:client_id/subscribe`.
  - Whether to call `DELETE /apps/:client_id/unsubscribe` vs `DELETE /apps/:client_id/unsubscribe/:id`.
- For `**one_per_client**`:
  - Do **not** invent per-subscription URLs; rely on `Client.webhooksURL`.
  - Treat `UserApp` as a single per-user configuration object.
- For `**one_per_subscription`\*\*:
  - Treat `subscriptions` as the **list of event types** for that subscription row (multiple allowed); duplicate **full** identity on POST returns **400**.
  - Persist the returned subscription identifier (`SubscribedUser.id` / `UserApp._id`) so you can call:
    - `PUT /apps/:client_id/subscriptions/:id` to update in place, or
    - `DELETE /apps/:client_id/unsubscribe/:id` to remove.
  - Do **not** rely on repeated `POST .../subscribe` to update an existing subscription (duplicates → **400**; partial changes → **extra rows**).
  - For `cv-trigger-action-integration`:
    - Prefer `subscription_actions` and flags like `run_on_short_message` instead of `webhookURL`.

Following these rules will keep generated integrations consistent with the current implementation in:

- `src/oauth2/schema/client.schema.ts`
- `src/user-app/user-app.schema.ts`
- `src/user-app/user-app.service.ts`
- `src/user-app/user-app.controller.ts`
