- Transaction store — the state of sign-in / confirmation transactions (Transaction Engine).
- OpenIddict database — the OIDC server’s clients and tokens.
Transaction store
Configured inConfigureTransactionEngine. Pick one store.
Program.cs
UseEfCoreStore itself ships in a satellite package — the core does not drag EF Core in
transitively. The EF Core provider (UseNpgsql, UseSqlServer) and its NuGet package belong to
the host application — the core is provider-agnostic. The migrations assembly named in
MigrationsAssembly(...) ships as one more package — without it the schema is never created:MigrationsAssembly(...):
The audit trail keeps its migration history in a table of its own, so that it can share a database
with the transaction store: next to
MigrationsAssembly(...) in UseEfCoreSink, pass
MigrationsHistoryTable(AuditMigrationDefaults.MigrationsHistoryTable).
Redis parameters (RedisTransactionStoreOptions):
Configuration— the connection string (for example,localhost:6379).KeyPrefix— the transaction key prefix (defaultveriqa:tx:).
Veriqa:TransactionEngine:Store:Provider
is PostgreSQL, SqlServer or Redis, and Veriqa:TransactionEngine:Store:ConnectionString holds the
connection string of that store. On Redis it goes into Configuration, and
Veriqa:TransactionEngine:Store:KeyPrefix sets KeyPrefix. Without a connection string the store stays
in memory whatever the provider says. A Redis store lends nothing to the audit sink (see
Audit sink) and cannot hold the
collected claims store; readiness gets a redis-transactions check, and a
Redis unreachable at startup stops the container (unless the connection string sets
abortConnect=false: then the container starts and only readiness reports the outage).
A new sign-in closes the earlier ones
When a user starts a sign-in again and reaches the channel with the same account, the earlier sign-ins of that account to the same application that are still waiting for an answer are closed — see Starting the sign-in again. The transaction store is what finds them, so the upgrade touches each store in its own way:- InMemory — nothing to do.
- EF Core — the migration
AddSignInSubjectFingerprintadds one nullable column with a non-unique index to the transactions table. It ships in both migration packages and is applied like any other migration. Sign-ins written before it carry no fingerprint: they are not closed by a newer one and live until they expire. - Redis — next to the transactions, under the same
KeyPrefix, the store keeps one set of transaction identifiers per tenant, application and channel account. The key holds a hash, never the user’s identifier in the channel, and the set expires on its own. - A store of your own —
ITransactionStorehas a new member,GetPendingSignInsAsync, and it has no default implementation: your store stops compiling until it implements it. The member’s obligations are described in the README of theVeriqa.Core.TransactionEnginepackage, section “Obligations of GetPendingSignInsAsync”.
Channel stores: multiple replicas
The channel layer keeps several stores of its own. By default they are in-process — enough for a single replica, but not for two:
If you run more than one replica, wire the Redis implementations of all these stores with a single
call on the channel adapter builder:
Program.cs
UseRedisChannelStores call everything stays
in-process — the default implementations are never replaced behind your back.
Parameters (RedisChannelStoreOptions):
Configuration— the connection string. Leave it empty to deliberately reuse anIConnectionMultiplexerregistered in the container by someone else. Setting it while such a registration exists is refused: the host stops at startup with a configuration error naming both connections by their endpoints, logical database, TLS settings and sentinel master name (credentials are never printed) — otherwise the configured string would be silently ignored and the channel stores would land in someone else’s Redis. The check runs when the host starts, so the order of the two registrations does not matter.PromptKeyPrefix— prompt coordinate key prefix (defaultveriqa:ch:prompt:).EmailActionTokenKeyPrefix— action token key prefix (defaultveriqa:ch:email:token:).EmailPushCorrelationKeyPrefix— push correlation key prefix (defaultveriqa:ch:email:corr:).ProcessedMessageKeyPrefix— processed-message key prefix (defaultveriqa:ch:email:msg:).EmailClaimCompletionKeyPrefix— key prefix of the email completion store (defaultveriqa:ch:email:claim:). Addresses and channel accounts in its keys are SHA-256 fingerprints, and codes and link tokens are kept only as fingerprints.PhoneRequestCorrelationKeyPrefix— phone number request key prefix (defaultveriqa:ch:phone:). The rest of the key is a SHA-256 hash of the chat, so the channel user identifier is not stored in plain text.LinkHandleKeyPrefix— link handle key prefix (defaultveriqa:ch:link:). The rest of the key is a SHA-256 fingerprint of the handle; the handle itself is never stored.StartCodeKeyPrefix— key prefix of the short codes a sign-in is started by (defaultveriqa:ch:code:). The rest of the key is a SHA-256 hash; the code itself is never part of a key.ProcessedMessageRetention— how long a “message processed” record is kept (default 24 hours).
Record lifetimes come from the entities themselves: a token and a correlation live exactly until
their own
ExpiresAt, and prompt coordinates until the longest possible transaction has expired
plus the margin in which the expiry event is published (the coordinates are consumed by that
event, which the background cleanup sweep publishes once the transaction has expired, unless an
answer that came after the deadline has already published it). The single
exception is ProcessedMessageRetention — a processed-message record has no owning entity.Veriqa:ChannelStores:Redis:ConnectionString; without it they stay in process. The parameters above
are keys of the same section under the same names — Veriqa:ChannelStores:Redis:LinkHandleKeyPrefix,
Veriqa:ChannelStores:Redis:ProcessedMessageRetention (hh:mm:ss) and so on; the connection string
is ConnectionString, not Configuration. With the transaction store on Redis both share a single
connection: set the same connection string in both keys. A different one stops startup with a
message naming the two keys. Readiness gets a redis-channel-stores check unless the same Redis is
already probed by another check, and a Redis unreachable at startup stops the container (unless the
connection string sets abortConnect=false: then the container starts and only readiness reports the
outage).
Channel identities: nothing is stored out of the box
TheIChannelIdentityRepository port (transaction engine) persists a channel identity — channel
user id, phone, email, display name — as an upsert keyed by the triple (TenantId, ChannelType,
ChannelUserId). The contract provides neither deletion nor TTL: a record is kept indefinitely.
Veriqa ships no implementation of this port and registers none. Without a registration of your
own nothing is stored, and the sign-in still works: the resolved identity snapshot travels with the
transaction. Keeping that data (PII, kept indefinitely) is your decision — register an
implementation and every sign-in becomes an upsert:
Program.cs
Scoped implementation over a DbContext is as safe as a Singleton one. A
reference in-memory implementation lives in the samples/dotnet/inproc/login sample.
The full picture of what is kept where and for how long — this port included — is in
Security and data handling.
Collected claims store
When a sign-in lacks a claim the application requires, Veriqa collects it itself — a phone number requested in the channel, an email confirmed with a code or a link, a field of its form (see Claim enrichment). Without a store all of that lives only as long as the transaction, and the next sign-in asks again. The collected claims store keeps it keyed by the tenant and thesub: what Veriqa collected, the Optional claims the user declined, the time of
the last sign-in that read the records and the sub an application received when its claims mapper
changed it. Values the channel itself supplies are not stored.
Nothing is stored until you turn it on. On the standalone server that is one key,
Veriqa:CollectedClaims:Store:Enabled:
appsettings.json
Enabled: true without
Veriqa:TransactionEngine:Store:ConnectionString stops startup with a message naming both keys, and
so does Enabled: true with Veriqa:TransactionEngine:Store:Provider = Redis: Redis has no
relational database to keep the store in.
In the embedded mode the store is a registration of its own, and its database is yours to choose:
Program.cs
CollectedClaimsDbContext, and are applied at startup like the transaction store’s. Keep
MigrationsHistoryTable(CollectedClaimsMigrationDefaults.MigrationsHistoryTable) — the store may
share a database with the transactions, and a shared history table would mix the two sets of
migrations. Your own implementation of ICollectedClaimsStore is plugged in with
te.UseCollectedClaimsStore<TStore>() and gets everything below — the endpoint, the host call and
the retention sweep — exactly as the EF Core one does.
Deleting by sub
When a user deletes their account or asks for their data to be erased, you, as the data
controller, delete everything the store keeps about them — all their records and the subjects
issued to applications. One operation has two entry points; both are idempotent, so a
sub with nothing stored is a success.
The server endpoint — for an application that does not embed Veriqa as a .NET library:
AllowClientCredentials and AllowCollectedClaimsDeletion (see
Clients; the flag without AllowClientCredentials stops
startup), and its token comes from grant_type=client_credentials — as for the
confirmation API. sub is passed in the form the
application received it: the form of the application that client_id names, or of the calling
client when the parameter is absent. The subjects issued to that application are searched within the caller’s tenant; a
sub not found among them is taken as the core one. The endpoint has no rate limit of its own —
grant the flag to one of your own server clients only.
The host call — for an application that embeds Veriqa:
ICollectedClaimsDeletion is registered together with the store; without a store it is not in the
container.
Each deletion writes a CollectedClaimsDeleted record to the audit trail
when the trail is on: the actor is the calling client for the endpoint and system for the host
call and the sweep; the target is the masked core sub; the metadata carry the tenant, the client
and the basis — endpoint, host_call or retention. Claim values never reach the record. With
the trail off the deletion works and writes nothing. After a deletion the next sign-in of that user
works like the first one: Veriqa asks again for what is missing, a previously declined Optional
claim included.
Deleting unused records
Records of asub nobody has signed in with for a given number of days can be deleted
automatically. The clock starts at the last sign-in that read the records, not at the time a value
was collected. Off by default:
appsettings.json
0 turns it off; a negative value stops startup. At the tenant level the member
ClaimCompletionCollectedClaimsRetentionDays replaces the global value, so the sweep can run for one
tenant only (ClaimCompletion.CollectedClaimsRetentionDays in the
declaration catalog). The sweep runs at
startup and then hourly, in batches of 100; a sign-in that reads the records between the selection
and the deletion keeps them. Each deleted sub gets an audit record with the basis retention.
OpenIddict database
The OIDC server’s clients and tokens are stored separately and configured with theVeriqa:OpenIddict:Database section:
appsettings.json
Single-instance connector without a database
When Veriqa is attached to another identity provider — Keycloak or your own — it takes part in the sign-in and in nothing after it: the authorization request, the confirmation in the channel, the code exchange and the userinfo call right after it. The session and its refresh tokens then belong to that provider, and Veriqa keeps no user accounts either (see Channel identities). Such a deployment can run as a single instance with every store in memory. In self-hosted mode:appsettings.json
- leave
Veriqa:TransactionEngine:Store:ConnectionStringunset — the transactions stay in memory; - leave
Veriqa:Logging:Modeat its default, or at anything other thanAudit; - declare the client without
AllowRefreshTokens(off by default) and withoutoffline_access.
authServer.UseInMemoryOpenIddictStore() plus
te.UseInMemoryStore(). The host starts with a Warning that the OpenIddict store is volatile —
for this setup that is expected.
What a restart costs:
A deploy, an image update and a container restart are all restarts: each of them has every effect
listed in the table above.
Another database or a model change
The standalone image ships PostgreSQL and SQL Server (and Redis for the transaction store). Anything else is a choice your own host makes in the embedded mode — no change to Veriqa is needed.Your own migrations assembly
The four EF Core contexts are public:TransactionDbContext
(Veriqa.Core.TransactionEngine.Store.EfCore), CollectedClaimsDbContext
(Veriqa.Core.TransactionEngine.CollectedClaims.EfCore), AuditDbContext
(Veriqa.Core.AuditTrail.Store.EfCore) and OpenIddictDbContext (Veriqa.Core.AuthServer.Data). They are anchors for migrations, not a
data access API — their tables stay internal. For a database Veriqa ships no migrations for:
- Create a class library that references your EF Core provider,
Microsoft.EntityFrameworkCore.Designand the package holding the context. - Add an
IDesignTimeDbContextFactory<T>for each context —dotnet efbuilds the context through it. - Run
dotnet ef migrations add Initial --project <your library> --context <context>, and name the library inMigrationsAssembly(...)at runtime.
TransactionDbContextDesignTimeFactory.cs
AuditDbContext, add MigrationsHistoryTable(AuditMigrationDefaults.MigrationsHistoryTable) next
to MigrationsAssembly(...) — in the factory and at runtime alike; for CollectedClaimsDbContext,
MigrationsHistoryTable(CollectedClaimsMigrationDefaults.MigrationsHistoryTable) in the same way.
Changing the model
A schema, a collation, a column type — change the model with the standard EF CoreIModelCustomizer, not by editing Veriqa’s contexts. Derive from RelationalModelCustomizer:
VeriqaModelCustomizer.cs
ReplaceService<IModelCustomizer, …>() inside the same delegate that picks the
provider:
Program.cs
Publishing transaction events
In-process delivery toITransactionEventHandler (see
Configuration) is part of the engine and
needs no call of its own: the audit trail, the real-time sign-in push and the prompt cleanup
receive events in every deployment. Publishing events out of the process is a separate,
additive choice of a transport — also made in ConfigureTransactionEngine:
Program.cs
RabbitMqEventPublisherOptions: ConnectionString (required, amqp://…), ExchangeName
(default veriqa.transactions), plus queue and reconnection parameters (QueueCapacity,
MaxReconnectAttempts, ReconnectBaseDelaySeconds).
On the standalone image the publisher is switched on by Veriqa:Events:RabbitMq:ConnectionString;
the other parameters are keys of the same section under the same names
(Veriqa:Events:RabbitMq:ExchangeName and so on). Without the connection string there is no
publisher. With it, readiness gets a rabbitmq-events check, and a broker unreachable at startup
stops the container.
Reliability of what leaves the process is the reliability of the chosen bus — no more and no less.
Veriqa builds no delivery guarantee of its own on top of the transport: with the in-process channel
an event can be lost on a crash, and with a message queue delivery is exactly as reliable as that
queue makes it.
Next steps
Preparing for production
Migrations, secrets, key rotation.
Configuration & customization
Channels, design, localization, extension points.