Skip to main content
У Veriqa два независимых хранилища:
  • Хранилище транзакций — состояние транзакций входа/подтверждения (Transaction Engine).
  • База OpenIddict — клиенты и токены OIDC-сервера.
Третье — опциональное и по умолчанию выключено: хранилище собранных claims, которое помнит claims, собранные Veriqa во время входа.

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

Настраивается в ConfigureTransactionEngine. Выберите один стор.
Program.cs
Сам метод UseEfCoreStore приезжает сателлитным пакетом — ядро не тащит EF Core транзитивно. Провайдер EF Core (UseNpgsql, UseSqlServer) и его NuGet-пакет принадлежат хост-приложению — ядро провайдер-агностично. Сборка миграций, названная в MigrationsAssembly(...), приезжает ещё одним пакетом — без него схема не создастся:
Миграции поставляются пакетом на каждую пару «хранилище × провайдер»; имя пакета — оно же значение для MigrationsAssembly(...): Журнал аудита ведёт историю миграций в собственной таблице, чтобы делить БД с хранилищем транзакций: рядом с MigrationsAssembly(...) в UseEfCoreSink передайте MigrationsHistoryTable(AuditMigrationDefaults.MigrationsHistoryTable). Параметры Redis (RedisTransactionStoreOptions):
  • Configuration — строка подключения (например, localhost:6379).
  • KeyPrefix — префикс ключей транзакций (по умолчанию veriqa:tx:).

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

Канальные адаптеры держат пять собственных хранилищ. По умолчанию они in-process — этого достаточно для одной реплики, но не для двух: Разворачиваете больше одной реплики — подключите Redis-реализации всех пяти хранилищ одним вызовом на билдере канальных адаптеров:
Program.cs
Порядок вызовов значения не имеет: сателлит регистрирует пять хранилищ безусловно и выигрывает у 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 часа).
Срок жизни записей задаёт сама сущность: токен и корреляция живут ровно до своего ExpiresAt, координаты подсказки — до истечения самой долгой возможной транзакции плюс запас на публикацию события истечения (координаты потребляет это событие, а публикует его фоновая уборка уже после того, как транзакция истекла). Единственное исключение — ProcessedMessageRetention: у отметки о письме собственной сущности нет.
Проверьте Redis-хранилища в своём хосте после включения. Фолбэка на in-process нет: недоступный Redis даёт ошибку операции, а не молчаливый возврат к хранилищам, которые ломаются на второй реплике.

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

Порт IChannelIdentityRepository (движок транзакций) сохраняет идентичность канала — channel_user_id, телефон, email, отображаемое имя — upsert’ом по тройке (TenantId, ChannelType, ChannelUserId). Контракт не предусматривает ни удаления, ни TTL: запись живёт бессрочно. Реализации этого порта в поставке нет, и Veriqa её не регистрирует. Без вашей регистрации не сохраняется ничего, и вход при этом работает: с транзакцией едет снапшот резолюции идентичности. Хранить эти данные (PII, бессрочно) — ваше решение; зарегистрируйте свою реализацию, и каждый вход становится upsert’ом:
Program.cs
Время жизни реализации выбираете вы: резолвер спрашивает порт пооперационно из собственного скоупа, поэтому Scoped-реализация над DbContext безопасна наравне с Singleton. Референсная in-memory реализация лежит в сэмпле samples/dotnet/inproc/login. Полная картина «что где лежит и сколько живёт», включая этот порт, — в Безопасности и обработке данных.

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

Когда входу не хватает claim, который приложение требует, Veriqa добирает его сама — телефон запросом в канале, email кодом или ссылкой, поле своей формой (см. Обогащение claim’ов). Без хранилища всё это живёт столько же, сколько транзакция, и следующий вход спрашивает снова. Хранилище собранных claims держит это по тенанту и sub: что собрала Veriqa, от каких Optional claims пользователь отказался, время последнего входа, прочитавшего записи, и sub, который получило приложение, если его claims mapper этот sub поменял. Значения, которые даёт сам канал, не сохраняются. Пока вы его не включили, не хранится ничего. На отдельном сервере это один ключ, Veriqa:CollectedClaims:Store:Enabled:
appsettings.json
Хранилище тогда живёт в базе хранилища транзакций — тот же провайдер, строка подключения и сборка миграций — со своей таблицей истории миграций. Enabled: true без Veriqa:TransactionEngine:Store:ConnectionString останавливает старт с сообщением, называющим оба ключа. Во встроенном режиме хранилище — отдельная регистрация, и базу для него выбираете вы:
Program.cs
Миграции едут в пакетах миграций хранилища транзакций вторым контекстом, CollectedClaimsDbContext, и применяются на старте так же, как миграции транзакций. Не убирайте MigrationsHistoryTable(CollectedClaimsMigrationDefaults.MigrationsHistoryTable): хранилище может делить базу с транзакциями, и общая таблица истории смешала бы два набора миграций. Свою реализацию ICollectedClaimsStore подключает te.UseCollectedClaimsStore<TStore>() — и она получает всё описанное ниже: эндпоинт, вызов хоста и автоудаление — ровно как реализация на EF Core.
Хранилище одно на тенант и общее для всех его приложений: значение, собранное для одного приложения, выдаётся другому приложению того же тенанта, если проходит его правила, а удаление по запросу одного приложения стирает записи для всех.

Удаление по sub

Когда пользователь удалил аккаунт или потребовал стереть данные, вы как оператор персональных данных удаляете всё, что хранилище о нём держит, — все его записи и выданные приложениям sub. Операция одна, входов два; оба идемпотентны: sub, по которому ничего нет, — успех. Серверный эндпоинт — для приложения, которое не встраивает Veriqa как .NET-библиотеку:
Маршрут есть, только когда зарегистрировано хранилище собранных claims. Вызывает серверный клиент с AllowClientCredentials и AllowCollectedClaimsDeletion (см. Клиенты; флаг без AllowClientCredentials останавливает старт), токен — по grant_type=client_credentials, как у API подтверждений. sub передаётся в той форме, в какой его получило приложение: названное в client_id, а без параметра — вызывающий клиент. sub, выданные этому приложению, ищутся в тенанте вызывающего; не найденный среди них sub считается ядровым. Своего ограничения частоты у эндпоинта нет — выдавайте флаг только одному своему серверному клиенту. Вызов хоста — для приложения, в которое Veriqa встроена:
ICollectedClaimsDeletion регистрируется вместе с хранилищем; без хранилища его в контейнере нет. Каждое удаление пишет запись CollectedClaimsDeleted в журнал аудита, когда журнал включён: актор — вызывающий клиент для эндпоинта и system для вызова хоста и автоудаления; цель — маскированный ядровый sub; в метаданных — тенант, клиент и основание: endpoint, host_call или retention. Значения claims в запись не попадают. С выключенным журналом удаление работает и ничего не пишет. После удаления следующий вход этого пользователя идёт как первый: Veriqa снова спрашивает недостающее, включая Optional claim, от которого он раньше отказался.

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

Записи sub, по которому никто не входил заданное число дней, можно удалять автоматически. Срок отсчитывается от последнего входа, прочитавшего записи, а не от времени сбора значения. По умолчанию выключено:
appsettings.json
0 выключает; отрицательное значение останавливает старт. На уровне тенанта член ClaimCompletionCollectedClaimsRetentionDays замещает глобальное значение, так что очистку можно включить для одного тенанта (ClaimCompletion.CollectedClaimsRetentionDays в каталоге объявлений). Очистка идёт на старте и дальше раз в час, пачками по 100; вход, прочитавший записи между выборкой и удалением, их сохраняет. Каждый удалённый sub получает запись аудита с основанием retention.

База OpenIddict

Клиенты и токены OIDC-сервера хранятся отдельно и настраиваются секцией Veriqa:OpenIddict:Database:
appsettings.json
Умолчания у этого выбора нет. Пустая секция — не «InMemory по умолчанию», а несделанный выбор: вне окружения Development приложение не стартует (проверка на старте отказывает и называет лечение), в Development — поднимается с Warning. Во встроенном режиме секция провайдер не выбирает — там выбор произносит хост: authServer.UseOpenIddictDatabase(ef => ef.UseNpgsql(connectionString)) для реляционного провайдера либо authServer.UseInMemoryOpenIddictStore() для волатильного.
В продакшне инициализируйте реляционную БД миграциями, а не авто-созданием схемы — так у вас остаётся история схемы и предсказуемые апгрейды (см. Hardening).

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

Когда Veriqa подключена к другому провайдеру идентичности — Keycloak или вашему собственному, — она участвует во входе и ни в чём после него: запрос авторизации, подтверждение в канале, обмен кода и сразу за ним вызов userinfo. Сессия и её refresh-токены дальше принадлежат тому провайдеру, а аккаунтов пользователей Veriqa не хранит и так (см. Канальные идентичности). Такое развёртывание может работать одним инстансом со всеми хранилищами в памяти. В self-hosted режиме:
appsettings.json
  • Veriqa:TransactionEngine:Store:ConnectionString не задавайте — транзакции остаются в памяти;
  • Veriqa:Logging:Mode оставьте по умолчанию или задайте любым, кроме Audit;
  • клиента объявите без AllowRefreshTokens (по умолчанию выключен) и без offline_access.
Во встроенном режиме тот же выбор — authServer.UseInMemoryOpenIddictStore() и te.UseInMemoryStore(). Хост стартует с Warning о волатильном хранилище OpenIddict — для этого варианта это ожидаемо. Чего стоит рестарт: Деплой, обновление образа и перезапуск контейнера — всё это рестарты: каждый стоит первой строки таблицы.
Выберите базу данных, если:
  • Veriqa — единственный провайдер идентичности приложения, которое держит сессии на refresh-токенах (AllowRefreshTokens), — его пользователи входят заново после каждого рестарта;
  • инстансов больше одного — у каждого свои хранилища (см. Запуск более одного инстанса);
  • Veriqa:Logging:Mode = Audit — журнал в памяти вне Development не даёт хосту стартовать (см. Сток аудита).

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

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(...) в рантайме.
TransactionDbContextDesignTimeFactory.cs
Для AuditDbContext рядом с MigrationsAssembly(...) добавьте MigrationsHistoryTable(AuditMigrationDefaults.MigrationsHistoryTable) — и в фабрике, и в рантайме; для CollectedClaimsDbContext так же — MigrationsHistoryTable(CollectedClaimsMigrationDefaults.MigrationsHistoryTable).

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

Схема, collation, тип колонки — модель меняется штатным IModelCustomizer EF Core, а не правкой контекстов Veriqa. Унаследуйтесь от RelationalModelCustomizer:
VeriqaModelCustomizer.cs
Подключите его через ReplaceService<IModelCustomizer, …>() в том же делегате, что выбирает провайдер:
Program.cs
Для базы OpenIddict Veriqa применяет собственную настройку OpenIddict до вызова вашего делегата, поэтому сделанная в нём подмена и остаётся в силе. Изменённая модель уже не совпадает с поставляемыми миграциями, даже на PostgreSQL или SQL Server, — сгенерируйте для неё свою сборку, как описано выше.
Фабрика design-time обязана делать ту же подмену. dotnet ef строит модель через фабрику, приложение — через ваш делегат. Если ReplaceService<IModelCustomizer, …>() нет с одной из сторон, миграция описывает одну модель, а приложение работает с другой.
Не убирайте вызов base.Customize. Именно он выполняет OnModelCreating контекста. Без него в модели нет ни сущностей OpenIddict, ни собственной конфигурации таблиц Veriqa.

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

In-process доставка в ITransactionEventHandler (см. Настройку) — часть движка, отдельного вызова не требует: журнал аудита, push статуса на страницу входа и очистка промптов получают события в любой конфигурации. Публикация событий наружу — отдельный и дополняющий выбор транспорта, тоже в ConfigureTransactionEngine:
Program.cs
RabbitMqEventPublisherOptions: ConnectionString (обязателен, amqp://…), ExchangeName (по умолчанию veriqa.transactions), плюс параметры очереди и переподключения (QueueCapacity, MaxReconnectAttempts, ReconnectBaseDelaySeconds). Надёжность того, что уходит из процесса, равна надёжности выбранной шины — ни больше ни меньше. Своей гарантии доставки поверх транспорта Veriqa не строит: на in-process канале событие может потеряться при аварии, а с очередью сообщений доставка ровно такая, какую даёт эта очередь.

Дальше

Hardening к продакшну

Миграции, секреты, ротация ключей.

Настройка и кастомизация

Каналы, дизайн, локализация, точки расширения.