> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Хранилища

> Хранилище транзакций (InMemory / EF Core / Redis), база OpenIddict (PostgreSQL / SQL Server) и подключение другой СУБД.

У Veriqa два независимых хранилища:

* **Хранилище транзакций** — состояние транзакций входа/подтверждения (Transaction Engine).
* **База OpenIddict** — клиенты и токены OIDC-сервера.

Третье — опциональное и по умолчанию выключено:
[хранилище собранных claims](#хранилище-собранных-claims), которое помнит claims, собранные Veriqa
во время входа.

## Хранилище транзакций

Настраивается в `ConfigureTransactionEngine`. Выберите **один** стор.

```csharp Program.cs theme={null}
authServer.ConfigureTransactionEngine(te =>
{
    // Разработка / тесты — в памяти:
    te.UseInMemoryStore();

    // Продакшн — EF Core (провайдер и строка подключения принадлежат хосту):
    // te.UseEfCoreStore(ef => ef.UseNpgsql(connectionString,
    //     npgsql => npgsql.MigrationsAssembly("Veriqa.Core.TransactionEngine.Migrations.PostgreSql")));
    // …или на SQL Server:
    // te.UseEfCoreStore(ef => ef.UseSqlServer(connectionString,
    //     sql => sql.MigrationsAssembly("Veriqa.Core.TransactionEngine.Migrations.SqlServer")));

    // Redis (нативное истечение по TTL):
    // te.UseRedisStore(redis =>
    // {
    //     redis.Configuration = "localhost:6379";
    //     redis.KeyPrefix = "veriqa:tx:";
    // });
});
```

| Стор | Метод | Когда |
| - | - | - |
| InMemory | `UseInMemoryStore()` | Разработка, тесты и [коннектор на одном инстансе](#коннектор-на-одном-инстансе-без-базы-данных) (состояние теряется при рестарте). |
| EF Core | `UseEfCoreStore(ef => …)` | Продакшн на реляционной БД (PostgreSQL, SQL Server). |
| Redis | `UseRedisStore(o => …)` | Когда нужен внешний быстрый стор с нативным TTL. |

<Note>
  Сам метод `UseEfCoreStore` приезжает сателлитным пакетом — ядро не тащит EF Core транзитивно.
  Провайдер EF Core (`UseNpgsql`, `UseSqlServer`) и его NuGet-пакет принадлежат хост-приложению —
  ядро провайдер-агностично. Сборка миграций, названная в `MigrationsAssembly(...)`, приезжает
  ещё одним пакетом — без него схема не создастся:

  ```bash theme={null}
  dotnet add package Veriqa.Core.TransactionEngine.EntityFrameworkCore
  dotnet add package Veriqa.Core.TransactionEngine.Migrations.PostgreSql   # или …Migrations.SqlServer
  ```
</Note>

Миграции поставляются пакетом на каждую пару «хранилище × провайдер»; имя пакета — оно же значение
для `MigrationsAssembly(...)`:

| Хранилище | PostgreSQL | SQL Server |
| - | - | - |
| Транзакции и хранилище собранных claims | `Veriqa.Core.TransactionEngine.Migrations.PostgreSql` | `Veriqa.Core.TransactionEngine.Migrations.SqlServer` |
| OpenIddict | `Veriqa.Core.AuthServer.Migrations.PostgreSql` | `Veriqa.Core.AuthServer.Migrations.SqlServer` |
| Журнал аудита | `Veriqa.Core.AuditTrail.Migrations.PostgreSql` | `Veriqa.Core.AuditTrail.Migrations.SqlServer` |

Журнал аудита ведёт историю миграций в собственной таблице, чтобы делить БД с хранилищем транзакций:
рядом с `MigrationsAssembly(...)` в `UseEfCoreSink` передайте
`MigrationsHistoryTable(AuditMigrationDefaults.MigrationsHistoryTable)`.

Параметры Redis (`RedisTransactionStoreOptions`):

* `Configuration` — строка подключения (например, `localhost:6379`).
* `KeyPrefix` — префикс ключей транзакций (по умолчанию `veriqa:tx:`).

## Канальные хранилища: несколько реплик

Канальные адаптеры держат пять собственных хранилищ. По умолчанию они **in-process** — этого
достаточно для одной реплики, но не для двух:

| Порт | Что в нём лежит | Что ломается на второй реплике |
| - | - | - |
| `IEmailActionTokenStore` | action-токены Email Pull (magic link) | ссылка, открытая на другой реплике, токена не находит — вход не завершается |
| `IEmailPushCorrelationStore` | корреляции Email Push и дедупликация входящих писем | письмо не соотносится с транзакцией; повторная доставка не отсекается |
| `IChannelPromptMessageStore` | координаты отправленной in-channel подсказки | подсказка не редактируется по истечении TTL, если событие обработала не та реплика, что отправляла |
| `IPhoneRequestCorrelationStore` | к какой транзакции относится последний [запрос номера телефона](/docs/ru/guides/phone-number) в чате | присланный номер, попавший на другую реплику, запроса не находит — вход его не получает |
| хранилище добора email (внутреннее у Email-адаптера) | коды и ссылки писем, подтверждающих адрес почты на веб-шаге добора claims, и счётчики этих писем | код, отправленный одной репликой, другая отвергает; лимиты писем считаются по каждой реплике отдельно |

Разворачиваете больше одной реплики — подключите Redis-реализации всех пяти хранилищ одним
вызовом на билдере канальных адаптеров:

```bash theme={null}
dotnet add package Veriqa.Core.ChannelAdapter.Redis
```

```csharp Program.cs theme={null}
authServer.AddChannelAdapters(adapters =>
{
    adapters.AddEmail();

    adapters.UseRedisChannelStores(redis =>
    {
        // Пусто — переиспользовать IConnectionMultiplexer, уже зарегистрированный
        // в контейнере (например, сателлитом движка транзакций).
        redis.Configuration = "localhost:6379";
    });
});
```

Порядок вызовов значения не имеет: сателлит регистрирует пять хранилищ безусловно и выигрывает
у in-process-умолчаний в обе стороны. Без вызова `UseRedisChannelStores` всё остаётся
in-process — реализации по умолчанию не подменяются.

Параметры (`RedisChannelStoreOptions`):

* `Configuration` — строка подключения. **Оставьте пустой**, чтобы намеренно переиспользовать
  `IConnectionMultiplexer`, зарегистрированный в контейнере кем-то ещё. Задать её при таком
  мультиплексоре нельзя: хост падает на старте ошибкой конфигурации, называющей обе стороны их
  адресами, номером логической БД, настройками TLS и именем мастера sentinel (учётные данные не
  печатаются), — иначе заданная строка молча игнорировалась бы, а канальные хранилища уехали бы в
  чужой Redis. Проверка выполняется на старте хоста, поэтому
  порядок двух регистраций значения не имеет.
* `PromptKeyPrefix` — префикс координат подсказки (по умолчанию `veriqa:ch:prompt:`).
* `EmailActionTokenKeyPrefix` — префикс action-токенов (по умолчанию `veriqa:ch:email:token:`).
* `EmailPushCorrelationKeyPrefix` — префикс корреляций Push (по умолчанию `veriqa:ch:email:corr:`).
* `ProcessedMessageKeyPrefix` — префикс отметок обработанных писем (по умолчанию `veriqa:ch:email:msg:`).
* `EmailClaimCompletionKeyPrefix` — префикс хранилища добора email (по умолчанию
  `veriqa:ch:email:claim:`). Адреса и учётки канала в его ключах — отпечатки SHA-256, коды и токены
  ссылок хранятся только отпечатками.
* `PhoneRequestCorrelationKeyPrefix` — префикс запросов номера телефона (по умолчанию `veriqa:ch:phone:`).
  Остаток ключа — хеш SHA-256 чата, так что идентификатор пользователя канала открытым текстом не
  хранится.
* `ProcessedMessageRetention` — срок хранения отметки «письмо обработано» (по умолчанию 24 часа).

<Note>
  Срок жизни записей задаёт сама сущность: токен и корреляция живут ровно до своего
  `ExpiresAt`, координаты подсказки — до истечения самой долгой возможной транзакции плюс запас на
  публикацию события истечения (координаты потребляет это событие, а публикует его фоновая уборка
  уже после того, как транзакция истекла). Единственное исключение — `ProcessedMessageRetention`:
  у отметки о письме собственной сущности нет.
</Note>

<Warning>
  Проверьте Redis-хранилища в своём хосте после включения. Фолбэка на in-process нет:
  недоступный Redis даёт ошибку операции, а не молчаливый возврат к хранилищам, которые ломаются
  на второй реплике.
</Warning>

## Канальные идентичности: в поставке не хранятся

Порт `IChannelIdentityRepository` (движок транзакций) сохраняет идентичность канала —
`channel_user_id`, телефон, email, отображаемое имя — upsert'ом по тройке (`TenantId`,
`ChannelType`, `ChannelUserId`). Контракт не предусматривает ни удаления, ни TTL: запись живёт
бессрочно.

**Реализации этого порта в поставке нет**, и Veriqa её не регистрирует. Без вашей регистрации не
сохраняется ничего, и вход при этом работает: с транзакцией едет снапшот резолюции идентичности.
Хранить эти данные (PII, бессрочно) — ваше решение; зарегистрируйте свою реализацию, и каждый вход
становится upsert'ом:

```csharp Program.cs theme={null}
builder.Services.AddSingleton<IChannelIdentityRepository, MyChannelIdentityRepository>();
```

Время жизни реализации выбираете вы: резолвер спрашивает порт пооперационно из собственного
скоупа, поэтому `Scoped`-реализация над `DbContext` безопасна наравне с `Singleton`. Референсная
in-memory реализация лежит в сэмпле `samples/dotnet/inproc/login`.

Полная картина «что где лежит и сколько живёт», включая этот порт, — в
[Безопасности и обработке данных](/docs/ru/guides/data-handling#сроки-хранения).

## Хранилище собранных claims

Когда входу не хватает claim, который приложение требует, Veriqa добирает его сама — телефон
запросом в канале, email кодом или ссылкой, поле своей формой (см.
[Обогащение claim'ов](/docs/ru/concepts/claim-enrichment)). Без хранилища всё это живёт столько же,
сколько транзакция, и следующий вход спрашивает снова. Хранилище собранных claims держит это по
тенанту и `sub`: что собрала Veriqa, от каких `Optional` claims пользователь отказался, время
последнего входа, прочитавшего записи, и `sub`, который получило приложение, если его claims mapper
этот `sub` поменял. Значения, которые даёт сам канал, не сохраняются.

**Пока вы его не включили, не хранится ничего.** На отдельном сервере это один ключ,
`Veriqa:CollectedClaims:Store:Enabled`:

```json appsettings.json theme={null}
{
  "Veriqa": {
    "CollectedClaims": {
      "Store": { "Enabled": true }
    }
  }
}
```

Хранилище тогда живёт в базе хранилища транзакций — тот же провайдер, строка подключения и сборка
миграций — со своей таблицей истории миграций. `Enabled: true` без
`Veriqa:TransactionEngine:Store:ConnectionString` останавливает старт с сообщением, называющим оба
ключа.

Во встроенном режиме хранилище — отдельная регистрация, и базу для него выбираете вы:

```csharp Program.cs theme={null}
authServer.ConfigureTransactionEngine(te =>
{
    te.UseEfCoreStore(ef => ef.UseNpgsql(connectionString,
        npgsql => npgsql.MigrationsAssembly("Veriqa.Core.TransactionEngine.Migrations.PostgreSql")));

    te.UseEfCoreCollectedClaimsStore(ef => ef.UseNpgsql(connectionString, npgsql => npgsql
        .MigrationsAssembly("Veriqa.Core.TransactionEngine.Migrations.PostgreSql")
        .MigrationsHistoryTable(CollectedClaimsMigrationDefaults.MigrationsHistoryTable)));
});
```

Миграции едут в пакетах миграций хранилища транзакций вторым контекстом, `CollectedClaimsDbContext`,
и применяются на старте так же, как миграции транзакций. Не убирайте
`MigrationsHistoryTable(CollectedClaimsMigrationDefaults.MigrationsHistoryTable)`: хранилище может
делить базу с транзакциями, и общая таблица истории смешала бы два набора миграций. Свою реализацию
`ICollectedClaimsStore` подключает `te.UseCollectedClaimsStore<TStore>()` — и она получает всё
описанное ниже: эндпоинт, вызов хоста и автоудаление — ровно как реализация на EF Core.

<Warning>
  Хранилище **одно на тенант и общее для всех его приложений**: значение, собранное для одного
  приложения, выдаётся другому приложению того же тенанта, если проходит его правила, а удаление по запросу одного приложения
  стирает записи для всех.
</Warning>

### Удаление по `sub`

Когда пользователь удалил аккаунт или потребовал стереть данные, вы как оператор персональных
данных удаляете всё, что хранилище о нём держит, — все его записи и выданные приложениям `sub`. Операция одна, входов два; оба идемпотентны: `sub`, по которому ничего нет, — успех.

**Серверный эндпоинт** — для приложения, которое не встраивает Veriqa как .NET-библиотеку:

```http theme={null}
DELETE /api/collected-claims?sub=<sub>[&client_id=<приложение>]
Authorization: Bearer <токен client credentials>
```

Маршрут есть, только когда зарегистрировано хранилище собранных claims. Вызывает серверный клиент с
`AllowClientCredentials` и `AllowCollectedClaimsDeletion` (см. [Клиенты](/docs/ru/reference/configuration#clients);
флаг без `AllowClientCredentials` останавливает старт), токен — по `grant_type=client_credentials`,
как у [API подтверждений](/docs/ru/reference/confirmation-api#2-получите-токен). `sub` передаётся в той
форме, в какой его получило приложение: названное в `client_id`, а без параметра — вызывающий
клиент. `sub`, выданные этому приложению, ищутся в тенанте вызывающего; не найденный среди них
`sub` считается ядровым. Своего ограничения частоты у эндпоинта нет — выдавайте флаг только одному
своему серверному клиенту.

| Условие | Ответ |
| - | - |
| удалено или ничего не было | `204 No Content` |
| нет токена или он недействителен | `401` |
| токен пользователя или клиент без `AllowCollectedClaimsDeletion` | `403` |
| `sub` нет, он пуст или передан дважды; `client_id` пуст или передан дважды | `400` problem, `title` = `oidc_request_invalid` |
| сбой хранилища или определения тенанта | `503` problem, `title` = `collected_claims_store_unavailable`. Сбой определения тенанта ничего не удаляет. Если за `sub` стоят несколько ядровых `sub`, удалённые до сбоя хранилища остаются удалёнными и записанными в журнал аудита; повтор запроса довершает удаление |

**Вызов хоста** — для приложения, в которое Veriqa встроена:

```csharp theme={null}
// tenantId: null — неявный тенант self-hosted установки.
// applicationId: client_id, которому выдан subject; null — subject и есть ядровый sub.
await deletion.DeleteBySubjectAsync(tenantId: null, applicationId: "shop", subject: sub);
```

`ICollectedClaimsDeletion` регистрируется вместе с хранилищем; без хранилища его в контейнере нет.

Каждое удаление пишет запись `CollectedClaimsDeleted` в [журнал аудита](/docs/ru/quickstart/self-hosted#сток-аудита),
когда журнал включён: актор — вызывающий клиент для эндпоинта и `system` для вызова хоста и
автоудаления; цель — маскированный ядровый `sub`; в метаданных — тенант, клиент и основание:
`endpoint`, `host_call` или `retention`. Значения claims в запись не попадают. С выключенным журналом
удаление работает и ничего не пишет. После удаления следующий вход этого пользователя идёт как
первый: Veriqa снова спрашивает недостающее, включая `Optional` claim, от которого он раньше
отказался.

### Удаление неиспользуемых записей

Записи `sub`, по которому никто не входил заданное число дней, можно удалять автоматически. Срок
отсчитывается от последнего входа, прочитавшего записи, а не от времени сбора значения. По
умолчанию выключено:

```json appsettings.json theme={null}
{
  "Veriqa": {
    "ClaimCompletion": {
      "CollectedClaims": { "RetentionDays": 365 }
    }
  }
}
```

`0` выключает; отрицательное значение останавливает старт. На уровне тенанта член
`ClaimCompletionCollectedClaimsRetentionDays` замещает глобальное значение, так что очистку можно
включить для одного тенанта (`ClaimCompletion.CollectedClaimsRetentionDays` в
[каталоге объявлений](/docs/ru/reference/configuration#ключи-в-каталоге-объявлений)). Очистка идёт на
старте и дальше раз в час, пачками по 100; вход, прочитавший записи между выборкой и удалением,
их сохраняет. Каждый удалённый `sub` получает запись аудита с основанием `retention`.

## База OpenIddict

Клиенты и токены OIDC-сервера хранятся отдельно и настраиваются секцией
`Veriqa:OpenIddict:Database`:

```json appsettings.json theme={null}
{
  "Veriqa": {
    "OpenIddict": {
      "Database": {
        "Provider": "PostgreSQL",
        "ConnectionString": "Host=…;Database=…;Username=…;Password=…"
      }
    }
  }
}
```

| Provider | Назначение |
| - | - |
| `InMemory` | Разработка, тесты и [коннектор на одном инстансе](#коннектор-на-одном-инстансе-без-базы-данных). `ConnectionString` не нужен. |
| `PostgreSQL` | Продакшн-провайдер. `ConnectionString` обязателен. |
| `SqlServer` | Продакшн-провайдер. `ConnectionString` обязателен. |

<Warning>
  **Умолчания у этого выбора нет.** Пустая секция — не «`InMemory` по умолчанию», а несделанный
  выбор: вне окружения `Development` приложение не стартует (проверка на старте отказывает и
  называет лечение), в `Development` — поднимается с `Warning`. Во встроенном режиме секция
  провайдер не выбирает — там выбор произносит хост:
  `authServer.UseOpenIddictDatabase(ef => ef.UseNpgsql(connectionString))` для реляционного
  провайдера либо `authServer.UseInMemoryOpenIddictStore()` для волатильного.
</Warning>

<Warning>
  В продакшне инициализируйте реляционную БД **миграциями**, а не авто-созданием схемы — так у
  вас остаётся история схемы и предсказуемые апгрейды (см. [Hardening](/docs/ru/guides/hardening)).
</Warning>

## Коннектор на одном инстансе без базы данных

Когда Veriqa подключена к другому провайдеру идентичности — [Keycloak](/docs/ru/integrations/apps#keycloak)
или вашему собственному, — она участвует во входе и ни в чём после него: запрос авторизации,
подтверждение в канале, обмен кода и сразу за ним вызов userinfo. Сессия и её refresh-токены дальше
принадлежат тому провайдеру, а аккаунтов пользователей Veriqa не хранит и так (см.
[Канальные идентичности](#канальные-идентичности-в-поставке-не-хранятся)). Такое развёртывание может
работать одним инстансом со всеми хранилищами в памяти.

В self-hosted режиме:

```json appsettings.json theme={null}
{
  "Veriqa": {
    "OpenIddict": {
      "Database": { "Provider": "InMemory" }
    }
  }
}
```

* `Veriqa:TransactionEngine:Store:ConnectionString` не задавайте — транзакции остаются в памяти;
* `Veriqa:Logging:Mode` оставьте по умолчанию или задайте любым, кроме `Audit`;
* клиента объявите без `AllowRefreshTokens` (по умолчанию выключен) и без `offline_access`.

Во встроенном режиме тот же выбор — `authServer.UseInMemoryOpenIddictStore()` и
`te.UseInMemoryStore()`. Хост стартует с `Warning` о волатильном хранилище OpenIddict — для этого
варианта это ожидаемо.

Чего стоит рестарт:

| Что | После рестарта |
| - | - |
| Входы в процессе | Не завершаются; пользователь начинает вход заново |
| Сессии в вашем провайдере идентичности | Не затронуты — в Veriqa они и не жили |
| Access-токены, выданные до рестарта | Отклоняются: access token — непрозрачная ссылка на запись в хранилище OpenIddict, а записи больше нет — не важно, если ваш провайдер вызывает userinfo сразу после обмена кода |
| Refresh-токены | Нет — клиент их не получает |

Деплой, обновление образа и перезапуск контейнера — всё это рестарты: каждый стоит первой строки таблицы.

<Warning>
  **Выберите базу данных, если:**

  * Veriqa — единственный провайдер идентичности приложения, которое держит сессии на refresh-токенах
    (`AllowRefreshTokens`), — его пользователи входят заново после каждого рестарта;
  * инстансов больше одного — у каждого свои хранилища (см.
    [Запуск более одного инстанса](/docs/ru/quickstart/self-hosted#запуск-более-одного-инстанса));
  * `Veriqa:Logging:Mode` = `Audit` — журнал в памяти вне `Development` не даёт хосту стартовать (см.
    [Сток аудита](/docs/ru/quickstart/self-hosted#сток-аудита)).
</Warning>

## Другая СУБД и правка модели

Standalone-образ несёт PostgreSQL и SQL Server. Всё остальное — выбор вашего хоста во встроенном
режиме, правки Veriqa для этого не нужны.

### Своя сборка миграций

Четыре контекста EF Core публичны: `TransactionDbContext`
(`Veriqa.Core.TransactionEngine.Store.EfCore`), `CollectedClaimsDbContext`
(`Veriqa.Core.TransactionEngine.CollectedClaims.EfCore`), `AuditDbContext`
(`Veriqa.Core.AuditTrail.Store.EfCore`) и `OpenIddictDbContext` (`Veriqa.Core.AuthServer.Data`). Это якоря для миграций, а не API доступа к
данным — таблицы остаются внутренними. Для СУБД, под которую Veriqa миграций не поставляет:

1. Создайте библиотеку классов со ссылками на ваш провайдер EF Core,
   `Microsoft.EntityFrameworkCore.Design` и пакет, в котором лежит контекст.
2. Добавьте `IDesignTimeDbContextFactory<T>` на каждый контекст — через неё `dotnet ef` строит контекст.
3. Выполните `dotnet ef migrations add Initial --project <ваша библиотека> --context <контекст>` и
   назовите библиотеку в `MigrationsAssembly(...)` в рантайме.

```csharp TransactionDbContextDesignTimeFactory.cs theme={null}
public sealed class TransactionDbContextDesignTimeFactory
    : IDesignTimeDbContextFactory<TransactionDbContext>
{
    public TransactionDbContext CreateDbContext(string[] args)
    {
        var options = new DbContextOptionsBuilder<TransactionDbContext>()
            // UseXxx — ваш провайдер EF Core.
            .UseXxx(Environment.GetEnvironmentVariable("TRANSACTION_DB_CONNECTION_STRING"),
                db => db.MigrationsAssembly(typeof(TransactionDbContextDesignTimeFactory).Assembly.GetName().Name))
            // Только если вы правите модель — та же подмена, что в рантайме (см. ниже).
            .ReplaceService<IModelCustomizer, VeriqaModelCustomizer>()
            .Options;

        return new TransactionDbContext(options);
    }
}
```

Для `AuditDbContext` рядом с `MigrationsAssembly(...)` добавьте
`MigrationsHistoryTable(AuditMigrationDefaults.MigrationsHistoryTable)` — и в фабрике, и в рантайме;
для `CollectedClaimsDbContext` так же —
`MigrationsHistoryTable(CollectedClaimsMigrationDefaults.MigrationsHistoryTable)`.

### Правка модели

Схема, collation, тип колонки — модель меняется штатным `IModelCustomizer` EF Core, а не правкой
контекстов Veriqa. Унаследуйтесь от `RelationalModelCustomizer`:

```csharp VeriqaModelCustomizer.cs theme={null}
public sealed class VeriqaModelCustomizer(ModelCustomizerDependencies dependencies)
    : RelationalModelCustomizer(dependencies)
{
    public override void Customize(ModelBuilder modelBuilder, DbContext context)
    {
        // Выполняет OnModelCreating контекста: сущности и конфигурацию Veriqa.
        base.Customize(modelBuilder, context);

        // Ваши изменения поверх — например, собственная схема:
        modelBuilder.HasDefaultSchema("veriqa");
    }
}
```

Подключите его через `ReplaceService<IModelCustomizer, …>()` в том же делегате, что выбирает
провайдер:

```csharp Program.cs theme={null}
authServer.ConfigureTransactionEngine(te =>
    te.UseEfCoreStore(ef => ef
        .UseXxx(connectionString, db => db.MigrationsAssembly("MyCompany.Veriqa.Migrations"))
        .ReplaceService<IModelCustomizer, VeriqaModelCustomizer>()));

authServer.UseOpenIddictDatabase(ef => ef
    .UseXxx(connectionString, db => db.MigrationsAssembly("MyCompany.Veriqa.Migrations"))
    .ReplaceService<IModelCustomizer, VeriqaModelCustomizer>());

services.AddVeriqaAuditTrail(audit =>
    audit.UseEfCoreSink(ef => ef
        .UseXxx(connectionString, db =>
        {
            db.MigrationsAssembly("MyCompany.Veriqa.Migrations");
            db.MigrationsHistoryTable(AuditMigrationDefaults.MigrationsHistoryTable);
        })
        .ReplaceService<IModelCustomizer, VeriqaModelCustomizer>()));
```

Для базы OpenIddict Veriqa применяет собственную настройку OpenIddict до вызова вашего делегата,
поэтому сделанная в нём подмена и остаётся в силе. Изменённая модель уже не совпадает с поставляемыми
миграциями, даже на PostgreSQL или SQL Server, — сгенерируйте для неё свою сборку, как описано выше.

<Warning>
  **Фабрика design-time обязана делать ту же подмену.** `dotnet ef` строит модель через фабрику,
  приложение — через ваш делегат. Если `ReplaceService<IModelCustomizer, …>()` нет с одной из сторон,
  миграция описывает одну модель, а приложение работает с другой.
</Warning>

<Warning>
  **Не убирайте вызов `base.Customize`.** Именно он выполняет `OnModelCreating` контекста. Без него в
  модели нет ни сущностей OpenIddict, ни собственной конфигурации таблиц Veriqa.
</Warning>

## Публикация событий транзакций

In-process доставка в `ITransactionEventHandler` (см.
[Настройку](/docs/ru/guides/customization#обработчик-событий-транзакции)) — часть движка, отдельного
вызова не требует: журнал аудита, push статуса на страницу входа и очистка промптов получают
события в любой конфигурации. Публикация событий **наружу** — отдельный и дополняющий выбор
транспорта, тоже в `ConfigureTransactionEngine`:

```csharp Program.cs theme={null}
authServer.ConfigureTransactionEngine(te =>
{
    te.UseEfCoreStore(ef => ef.UseNpgsql(connectionString));

    // Опционально — параметры in-process диспетчеризации (ёмкость очереди):
    // te.UseChannelsPublisher(ch => ch.Capacity = 1000);

    // Наружу — RabbitMQ (для распределённых систем и внешних потребителей).
    // Дополняет, а не заменяет: in-process обработчики продолжают получать события.
    // te.UseRabbitMqPublisher(mq =>
    // {
    //     mq.ConnectionString = "amqp://user:pass@host:5672/";
    //     mq.ExchangeName = "veriqa.transactions";
    // });
});
```

`RabbitMqEventPublisherOptions`: `ConnectionString` (обязателен, `amqp://…`), `ExchangeName`
(по умолчанию `veriqa.transactions`), плюс параметры очереди и переподключения (`QueueCapacity`,
`MaxReconnectAttempts`, `ReconnectBaseDelaySeconds`).

Надёжность того, что уходит из процесса, равна надёжности выбранной шины — ни больше ни меньше.
Своей гарантии доставки поверх транспорта Veriqa не строит: на in-process канале событие может
потеряться при аварии, а с очередью сообщений доставка ровно такая, какую даёт эта очередь.

## Дальше

<CardGroup cols={2}>
  <Card title="Hardening к продакшну" icon="shield-check" href="/docs/ru/guides/hardening">
    Миграции, секреты, ротация ключей.
  </Card>

  <Card title="Настройка и кастомизация" icon="sliders" href="/docs/ru/guides/customization">
    Каналы, дизайн, локализация, точки расширения.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.