IConfiguration, so every setting can come from
appsettings.json, environment variables, user-secrets or any other provider. Everything Veriqa
owns lives under a single root key, Veriqa:
This reference answers “which setting controls it”. Two neighboring questions have pages of
their own: what the specifications give you is in
OIDC and OAuth 2.0 support, and what is missing is in
Known limitations.
In environment variables,
: becomes __ — Veriqa:OpenIddict:Database:Provider is
Veriqa__OpenIddict__Database__Provider.
One shell limitation is worth knowing here. A POSIX shell allows only letters, digits and _ in a
variable name, so export rejects the whole name as soon as any segment of the path carries a
- or a .: export Veriqa__ChannelDisplay__Hints__my-channel=... fails as an invalid
identifier. Where that segment comes from makes no difference. It may be a value you choose —
the channel type in Veriqa:AuthPageDesign:QrCode:PixelsPerModuleByChannel:<channel type> and in
Veriqa:ChannelDisplay:Hints:<channel type> (the channel type of an SPI adapter is allowed a
hyphen), or the record code in the Veriqa:UiConfigurations:Records catalog. It may just as well
be a fixed key name of the schema, which you do not choose at all — the message kind in
Veriqa:MessageTemplates:confirmation-prompt.
The variable name itself is perfectly legal, and the limitation is worked around with the usual
tools: Docker (-e), Kubernetes variables (env:), launchSettings.json and a plain
env NAME=VALUE command all accept such names, and .NET reads them as usual. A path whose segments
hold only letters, digits and _ is set from an environment variable in the usual way.
A configuration directory of its own. One variable does not name a setting but a place to read
settings from: VERIQA_CONFIG_DIRECTORY points at a directory, and the host layers the
appsettings.json and appsettings.<Environment>.json found there over the files shipped with the
program and under the environment variables. Both files are optional; a directory that does not
exist prevents startup, an empty one is not an error. This is how an installation made by the
OS-service installer keeps its configuration — /etc/veriqa on Linux,
%ProgramData%\Veriqa on Windows — where replacing the program cannot touch it.
Configuration is validated at startup. An unknown enum value, a relational provider without a
connection string or an enabled channel missing its token stops the application immediately
rather than failing later on a user’s first sign-in.
Veriqa:OpenIddict
OIDC server, its store and the registered clients.Database
Provider has no default: an unset value is not “InMemory by default”, it is a choice nobody
made. Outside the Development environment such a host fails to start (the startup check names
the fix); in Development it starts with a Warning. InMemory remains a valid value, but it is
set explicitly and every restart invalidates issued tokens — fine for development and for a
single-instance connector.
In embedded mode these keys do not select the provider: there your host selects it by calling
authServer.UseOpenIddictDatabase(...) or authServer.UseInMemoryOpenIddictStore(); in
self-hosted mode the choice is set with these keys.
Server
The behavior depends on the environment, and the difference matters:
Development— with no explicit certificates, development keys are generated. Convenient locally, but every restart produces new keys and multiple instances do not share them.- Any other environment — there are no development keys: the server refuses to start until both certificates (signing and encryption) are configured. There is no fallback to generation.
ASPNETCORE_ENVIRONMENT=Production, so it follows the second
rule. If you see generated development keys in a deployed installation, the process is running in
Development — worth fixing before rollout.
Clients
Veriqa:OpenIddict:Clients is an array of the applications allowed to sign users in:
Next to the clients array sits the deployment gate:
Veriqa:TransactionEngine
The sign-in transaction lifecycle.Store
See Storage for migrations and retention.
Veriqa:ScopesClaims
Which scopes exist, which claims they carry and which claims a sign-in requires.
Keys of a claim in
Claims:
Veriqa:AuthPageDesign
Look and feel of the sign-in page — full walkthrough in Configuration & customization.ChannelButton, ChannelLayout and Cancel are stated on the same ownership levels as the
QrCode section below — the ui_config record → the client
(Veriqa:OpenIddict:Clients[]) → this global section — and the nearest level that states a key wins
it on its own.
Veriqa:AuthPageDesign:QrCode
The QR code on the sign-in page: whether to show it and at which resolution to draw it.
The section is stated on three ownership levels, and the nearest level that states it wins: the
ui_config record → the client (Veriqa:OpenIddict:Clients[].QrCode) → this global section.
Showing the QR and its resolution are two different settings, and a level wins each of them on its
own: a client that states only ShowQrCode keeps the global resolution.
The resolution itself is ONE setting stated per channel: PixelsPerModuleByChannel and
PixelsPerModule are two entries of the same level, and a level exhausts both of them before the
lookup moves on to the level below. What that means in practice: a client that
states only PixelsPerModule wins the resolution for every channel — the per-channel map of the
global section is never reached. To keep a channel’s own resolution, name it in the map of the same
level that states PixelsPerModule.
An example. The global section states PixelsPerModule: 6 and a PixelsPerModuleByChannel entry
telegram: 14, while the app-client client states only PixelsPerModule: 8:
A
PixelsPerModule outside the range on the client or ui_config level does not apply — the
resolution comes from the level below and the host still starts. Such a value is reported by ONE
warning in the log per configuration snapshot: the whole catalog of values (the global section, the
client entries, the ui_config records) is checked at startup and on every configuration reload,
rather than on every request. In the global section such a value remains a deployment error and
prevents startup.
Veriqa:Localization and Veriqa:ChannelDisplay
Veriqa:RateLimit
Per-endpoint limits, each a permit count over a window in seconds. Defaults:
Webhook limits deserve attention when you scale: they apply per instance, and a channel provider
that retries aggressively can trip them.
Veriqa:InitiatorContext
Context about the device that started the transaction, shown to the user before they confirm — see Initiator context.Veriqa:HopMode
Hop mode puts a short one-time link to your Veriqa server into the QR code instead of the channel link. Two of its settings are set node-wide and may be overridden for a single channel:
A channel value, when set, wins over the node-wide one; an absent or empty channel value inherits it.
The remaining
HopMode.* keys and their defaults are listed in
Keys in the declaration catalog, where HopMode.Enabled and
HopMode.RedirectStyle show “not declared”: their defaults are the values above.
One QR code for the whole transaction. Veriqa:HopMode:QrMode decides what a sign-in window or a
confirmation page with several channels shows while hop mode is on for at least one of them:
A page of one channel, a page under
ShowQrCode: Never and a phone under DesktopOnly show no tabs
whatever the value.
In a desktop window wide enough for it, a page with the single QR code uses two columns — the QR code
on the left, the channel buttons on the right; a narrower window, and a page that shows no channel buttons
on the desktop (ShowChannelButton: MobileOnly or Never), keep one column.
Veriqa:HopMode:ShowChannelButtonsOnAnyChannelTab (default true) decides, under UnifiedWithChannels,
whether the “Any channel” tab shows the channel buttons beside the one QR code. With false that tab holds
the QR code alone and its instruction asks only to scan it; the tab of each channel keeps its button. A
phone under ShowQrCode: DesktopOnly shows no QR code, so it shows the buttons of every channel whatever
the value. Under PerChannel and Unified the key has no effect.
The Email channel under hop mode. The target of the Email channel is an email the phone itself
opens — as it is without hop mode. While the sign-in by an
emailed link is on (PullEnabled), the phone gets one page — the address field and the button that
opens the email — under either RedirectStyle, and the page never opens the mail client by itself.
With the emailed link off, Email follows RedirectStyle like any other channel: Interstitial gives
the launch page, which opens the email by itself and keeps a button in case it did not open; Direct
redirects straight to the email (302 to mailto:), and that redirect tells the sign-in page that
the email is open in the mail app. See
Email under hop mode.
Hop mode for one application
The same members can be stated inside the entry of an OIDC client (Veriqa:OpenIddict:Clients), and
they then apply to that application only:
appsettings.json
"HopEnabled": "maybe",
or a style name other than Interstitial / Direct — is skipped with a warning in the log, and the
next value in that order applies. A style stated as a number that names neither of the two is skipped
the same way, but without the warning. The other hop settings — what is collected about the
scanning device, TokenLifetime and HopPath — are set for the node only.
The endpoint behind the hop link (HopPath) exists only when hop mode is on at startup for a channel
that is enabled on the node — node-wide, for that channel, or in at least one client entry. Client
entries are read once at startup: if hop mode was off everywhere and you turn it on in a client
entry, restart the host. Whenever hop mode is on, Veriqa:OpenIddict:Server:Issuer has to be an
absolute https URL — in a client entry as well as on the node.
Infrastructure
Flat keys read by the host, mostly relevant when you run more than one instance or behind a proxy:
With neither DataProtection option set, keys live in memory and are lost on restart — fine for
development, broken for a scaled deployment.
Redis licensing. Redis server ≥ 7.4 ships under RSALv2/SSPL (8.x additionally offers AGPL).
That does not affect Veriqa’s license, but if it affects your policy, Valkey (BSD) is a
drop-in alternative.
Veriqa:Channels
Every channel lives underVeriqa:Channels:{Channel} and activates only with "Enabled": true. Full
setup with the provider-side steps is in Channel setup.
Two more keys go into the section of any channel, a channel of a third-party adapter included:
SpareStartCodeEnabled (bool, defaults to true) switches the spare start code the sign-in page
offers for a channel that keeps a working link, and BotName (text, not set by default) states the
name the user sees the chat under — it wins over the name the channel reports. Both are keys of the
core with the channel as a dimension, Channels.SpareStartCodeEnabled and Channels.BotName in the
catalog, stated in the configuration of the host or for a tenant.
What they change on the pages: Channel setup.
InboundVerification — a required key of every channel section
Veriqa:Channels:{Channel}:InboundVerification declares how the authenticity of an incoming event
of that channel is verified. The value comes from a closed dictionary:
You declare the fact, not the adapter and not the core: Veriqa only sees that a check ran and
passed, never what the check was — and deriving the value from the channel type would leave a
third-party channel with nothing to describe it. The value for every built-in channel and mode is in
Channel setup. A channel that fetches its updates itself
(
UpdateMode: Polling) while declaring anything other than outbound_fetch produces a Warning at
startup; startup continues.
The declared value reaches the audit record as an attribute of its own, so the audit log shows what a
confirmation was protected by.
OutcomeNotice — where the receipt of an outcome appears
Veriqa:Channels:OutcomeNotice:DisplayIntent (key Channels.OutcomeNotice.DisplayIntent) says
how you want the receipt of a terminal outcome to appear in the conversation. It is stated over
three levels — core, tenant and application — and each of them overrides the one below it freely: the
global section sets the intent of the whole installation, a tenant states its own with the member
OutcomeNoticeDisplayIntent of its record, and a single application with the member of the same name
in its OIDC client entry. A ui_config record states no intent. Whatever level answers, the value
covers the outcome as a whole — a confirmation, a decline and an expiry are all shown the same way.
The shipped value is
ReplacePrompt — what the channels did before the setting existed, so a
deployment that states nothing sees no change. An unreadable value is read as nothing stated and
answers ReplacePrompt too; startup is not interrupted.
This is an intent, not a guarantee. Telegram and MAX honor it. WhatsApp, Instagram, Messenger, WeChat and Viber always send a new
message, and Email shows no outcome receipt at all — as they ship today. An adapter that does not
honor the intent is not failing: nothing is logged as an error and the result of the delivery does
not change. With
NewMessage the receipt arrives as its own message and the buttons of the
question are taken away as part of the same delivery, where the platform can change a message’s
buttons without rewriting its text; the wording of the question is never rewritten. Failing to take
the buttons away is not a failure of the delivery either — and where a button does survive, pressing
it only earns another receipt, because a terminal transaction cannot be replayed.Veriqa:CompletionBehavior
What a channel does after a sign-in is completed. It is one action, stated for every channel at once or for a channel of its own, and — for the actions that show buttons — what those buttons are.
The action is stated on three ownership levels, and the nearest level that states it wins: the client
→ the tenant → this global section. Above the global section the same members sit in the
CompletionBehavior section of the level’s own record — for an application that is
Veriqa:OpenIddict:Clients[].CompletionBehavior:Action and …:ActionByChannel. A ui_config record
states no action: this is behavior, not presentation. The shipped value is None, so a deployment
that states nothing sees no change. A value the setting does not accept never interrupts startup,
and how far it reaches depends on its shape. A text that is not one of the names of the setting —
in Action or in an entry of ActionByChannel — leaves the whole level unstated: neither member of
that level is read, and the action comes from the level below. A number that names no action
discards only the entry it stands in: with Action: "WelcomeMessage" and
ActionByChannel: { "telegram": 7 } on one level, telegram takes that level’s WelcomeMessage.
The action is ONE setting stated per channel: ActionByChannel and Action are two entries of the
same level, and a level exhausts both of them before the lookup moves on to the level
below. So a client that states only Action wins the action for every channel — the per-channel
map of the global section is never reached. To keep a channel’s own action, name it in the map of the
same level that states Action.
telegram is followed by a welcome message, and a sign-in through any other
channel by nothing.
What a channel performs is a fact of the channel, not of your configuration:
An action the channel does not perform is never handed to it. It is replaced by the channel’s own
default —
None for every channel in the table — and the replacement is loud: a Warning in the
log and an audit event channel_completion_behavior_clamped whose details read
requested=<action>; applied=<action>. The sign-in itself completes as usual. The audit record
names the tenant and the client of the sign-in — the levels the action may have been stated on —
and, as its actor, the masked channel user. It carries the failure outcome, as every channel event
does, although the sign-in succeeded. None
is never replaced: doing nothing is honored on every channel, silently.
Menu and Redirect: a message with buttons
Menu and Redirect send one message: a text with buttons under it. Redirect is a single button
that opens a link; Menu is a list of buttons, one in a row, in the order you state them. The
channels that perform them are in the table above; on any other channel the action is replaced, as
described there.
telegram is followed by a menu of two buttons: the first opens a link, the
second is handed to your code. A redirect needs the address only — the label ships as Open:
MenuItems and RedirectUrl live on the
same three levels, each next to its own per-channel map, and a level exhausts its map and its plain
member before the level below is read — so a client that states only RedirectUrl wins the address
for every channel. RedirectButtonText is one value per level, with no per-channel map. A
ui_config record states none of them.
The labels are translated, the address is not touched. Text of an item and
RedirectButtonText are looked up in the locale files of your host in the language of the
recipient; a label no file carries is shown as you wrote it. The address is sent exactly as stated:
nothing about the user is substituted into it. The text above the buttons is a message of its own —
the kind post-login-menu or post-login-redirect, worded and overridden like any other message:
Veriqa:MessageTemplates.
What makes an item a link. An Action written as an absolute URL with scheme:// in front —
https://…, tg://… — becomes a link button. Any other value is callback data: the button
sends it back when pressed. The rule is the same one RedirectUrl is checked by. An address without
// — mailto:…, tel:… — is therefore read as callback data, not as a link. A value with a space
or a line break at either edge is not accepted at all — neither as RedirectUrl nor as an Action:
it is reported like any other value outside the setting’s domain, and an item written that way takes
its whole list with it. Whether a scheme is
allowed on a button is the platform’s decision: an address it refuses fails the send, and the message
does not go out.
A link can carry a link handle. Write {handle} — in exactly this case — into RedirectUrl, an
entry of RedirectUrlByChannel or the Action of a link item, for example
https://t.me/shop_bot?start={handle}. When the message is sent, every {handle} in its addresses is
replaced by one single-use value of 22 characters [0-9A-Za-z]; your server exchanges it for the
identity of the user who signed in —
Two bots. One message carries
one handle, valid for Veriqa:TransactionEngine:CompletedRetentionSeconds from the moment it is
issued. An address without the placeholder is sent as written and no handle is issued; there is no
separate setting to switch this on. In callback data {handle} is not replaced — the value goes out
as written. If the handle cannot be issued — the sign-in belongs to no client, or the handle store
refused the record — the buttons that ask for it are not sent and a Warning is written: Redirect
sends nothing, Menu goes out without those items. The sign-in is not affected.
A callback item is handed to your code. The press reaches the built-in channel adapter — the bot
is yours and Veriqa runs in the same process — and is passed to the handler you register:
IMenuItemPressHandler and MenuItemPress come with the Veriqa.Core.Contracts package. The handler
returns nothing: Veriqa has already acknowledged the press to the platform, and what it means is up to you.
The event names no sign-in — the menu is shown after the sign-in is over. The handler is created
when an item is pressed, in the scope of that event, and for no other event of the channel. An
exception — thrown by its constructor or by HandleAsync — is logged and affects neither the user
nor any sign-in.
Return quickly. HandleAsync is awaited while the event is being processed. Until it returns
the platform is not told that the event was received: a webhook left unanswered is delivered again, and your
handler gets the same press twice; in polling mode the events of other users wait behind it. Put
anything slow into a background queue of your own and return.
Treat press.Value as untrusted input. A button outlives the configuration that drew it: an item
you removed from MenuItems can still be pressed in an old message, and Veriqa does not check the
value against the items configured today. Check the value, and what this user may do, in your
handler.
Without a registered handler callback items are not drawn. A Warning names how many were left
out; the link items of the same menu are still sent. A press of a button sent before a restart that
dropped the handler is left without processing.
When nothing is sent. Menu with no items stated for the channel, Menu whose every item was
left out, and Redirect with no address stated each write a Warning and send nothing; the sign-in
completes as usual. A value the setting does not accept never interrupts startup and leaves its
entry unstated, so the value comes from the next entry or the level below: a RedirectUrl that is
not a URL, and a list in which any item has a blank Text or Action — one such item takes the
whole list of that entry out. An empty list states nothing either: to switch a menu off on a level
above, state the Action, not an empty list.
The platform’s budget is checked before the send, by the limits the platform itself names:
A message none of whose buttons can be drawn is not sent.
How a post-completion message behaves — the welcome message and a message with buttons alike:
- It follows every completed sign-in of a channel that performs the action — confirmed in the channel, confirmed on the web page, or completed by the steps that collect missing claims. A confirmation your server creates is not a sign-in and is followed by nothing.
- It arrives as a message of its own, after the outcome receipt, and never replaces it.
- A failure to send it is logged and does not change the result of the sign-in.
- The text of the welcome is the message kind
post-login-welcome— worded, localized and overridden like any other message: Veriqa:MessageTemplates. - Delivery is not recorded on the transaction. An event repeated in order earns no second message; the same event processed twice at the same moment may, as the product ships today.
CompletionBehavior.Action shows
not declared in the Default column: the shipped None is applied where no level states the action,
the same way as the shipped resolution of AuthPageDesign.QrCode.PixelsPerModule.
CompletionBehavior.MenuItems and CompletionBehavior.RedirectUrl show not declared for a plainer
reason: the product ships no menu and no address.
Veriqa:MessageTemplates
Everything the user reads in the messaging app and in email is a message. One message is two settings rather than one text: the contract declares which slots the message has, and the template carries the wordings that use those slots. Hence two addresses — one pair per message, not one line per text:
The bracketed groups are optional: a wording that does not narrow along an axis drops that group
whole — which is how one address serves every step of the lookup, from the most specific
(
{kind, transaction type, action type, surface, channel}) down to the broadest ({kind}). The kind
never drops out: “the text of which message” is the question itself.
One group is not independent: ByAction is read only under ByType. No step of the lookup names
an action type without its transaction type, so a wording at {kind}:ByAction:{action type} would
never be reached — the host refuses to start on it, saying the address is “narrowed by a combination
of axes no step of its ladder addresses”. A confirmation’s action type is therefore worded at
{kind}:ByType:confirmation:ByAction:{action type}.
The kinds the product ships:
A template’s value is a ladder of steps, fullest to minimal, as one array. A step is either a
string (a text that suits any sink) or a structure
{ "Subject": "…", "Html": "…", "Plain": "…" },
where Html and Plain are the editions of the step: an email is composed once and carries both of
its parts at once. Each edition is optional, but a step must state at least one — otherwise startup
fails, naming the message kind and the number of the step. The one shown is the first step that states
the edition asked for and has all of its slots filled, so the two parts of one email may come from
different steps, and the subject is taken from the step chosen for Html. An edition no step states is
one the email does not carry at all — that is how an HTML-only email is declared. The minimal step must
lean on guaranteed slots only — checked at startup, on every step and for every edition
separately. A declaration replaces the ladder whole: the steps stated at different addresses are never
merged.
A wording the product ships may sit at a narrower address than the one you write. The lookup goes
level by level, and a level exhausts every step of the address — from the most specific down to the
broadest — before the resolution drops to the level below. The shipped declarations are the fallback of
the Core level, read at the same addresses; and Veriqa:MessageTemplates in your
appsettings.json is that level. So a declaration of yours placed at a broader address than the
shipped one is never reached: the narrower step answers first, and it answers from the product. Nothing
fails and nothing is logged — your configuration is valid, it is simply never asked for. (A record
above the core is a different level and is read before it, so a wording in a client entry is not
shadowed this way — but it applies to that client alone.)
The outcome receipts are where this is met first. Each of the three terminal kinds ships a wording at the bare
{kind} and narrower ones for a sign-in:
outcome-receipt-declined ships the four addresses without ByDevice; outcome-receipt-expired
ships those four plus a step at ByType:login. A receipt of a sign-in shown in a messaging app asks
about {kind, login, in-channel-messenger} — and about the channel, which no shipped receipt narrows
by, and about the device the sign-in was started on — so restating
outcome-receipt-confirmed:Templates changes nothing there. Word it at the same addresses the
product used, the two device steps included:
appsettings.json
ByDevice: in the configuration of the host a shipped step that names the device is
asked before your step at the same address without it, so a declaration of
…:in-channel-messenger:Templates alone leaves phones and tablets with the shipped
Sign-in confirmed ✅ Tap Back to return to the sign-in page.. A declaration in a client entry, a
tenant record or a ui_config record is a level above the core and answers whole — no shipped step
is reached from there.
The contract stays at the root of the kind — it narrows by the transaction type and the action type and
never by the surface — and the ladder ends with a step using no slot, because no slot of a receipt is
guaranteed (below).
A receipt of a confirmation is a different address: the shipped file states nothing under
ByType:confirmation, so there the broad {kind} declaration is the step that answers and rewording it
works as written. Veriqa does not know the device a confirmation is answered on, so the device steps
never apply to it. If you want the receipt to tell the user how to get back to your page, word it
yourself with a caller slot whose value you pass in slot_values when you create the confirmation
(below). The surfaces a receipt is addressed with are in-channel-messenger (a channel that
renders emoji — the messaging apps the product ships an adapter for, and any channel declaring as much of
itself), in-channel-plain (a channel that does not), email-confirm-page (the confirmation page of
the Email channel), core-page (the confirmation page Veriqa serves itself) and interaction-page (the
page a confirmation with no browser callback leaves the user on).
Outcome receipts are plain text only. All four outcome-receipt-* kinds ship with one edition —
Plain — and the general rule “an edition no step states is one the email does not carry” does not
apply to them: a receipt has a single part, and not showing it means not answering the user at all.
So a receipt ladder overridden with the Html edition only yields no text whatsoever — that is a
broken installation rather than a different look: the render fails, naming the message kind, and no
other edition is substituted for it. Escaping a receipt on its way into a page is the page’s
job, so plain text takes nothing away from your wording.
A receipt can name what was confirmed. The shipped contract of the three receipt kinds declares
no slots, so the shipped receipts say nothing about the action. Declare caller slots in the contract of
a receipt on your level — under ByType:confirmation:ByAction:{action type} to word it per action —
and the receipt is filled from the slot_values the confirmation was created with, by the same names.
A caller slot of a receipt states MaxLength, as it does in the question:
Required. A receipt answering a decision from a channel the question
was never shown in gets no caller values, and neither does a receipt on the entry page of a
confirmation unless InteractionPage is in ConfirmationSubjectDisplay.Surfaces. The server slots
described below are filled there all the same, except outcome_at on the entry page.
A receipt can also carry what the product knows about the transaction. Its contract may declare
the server slots app, browser, os, region (type string, Source: ServerInitiatorContext) and
outcome_at (type datetime, Source: ServerSystem); a caller value never fills them. Naming one of those five Source: Caller instead is not refused at startup, but it does not hand you the slot: app is always the product’s, and the other four are yours only on the branches where the product has no value of its own. Use a name of your own instead. app names the
application: the client_id of a confirmation, the client’s display name (or its client_id) for a
sign-in. browser, os and region come from the initiator context, so only a sign-in has them, and
they obey the same Enabled and DisplayFields of Veriqa:InitiatorContext as the sign-in prompt.
outcome_at is the moment of the decision, or the deadline of an expired transaction; the entry page words
its receipts before the outcome, so there it has no value. None of these
slots is guaranteed in a receipt, even one marked Guaranteed, so the minimal step cannot rest on them.
The welcome message greets the user by name. The kind post-login-welcome is the text sent when
Veriqa:CompletionBehavior resolves to WelcomeMessage. Its shipped
contract declares one slot — name (type string, Source: ServerIdentityClaims), filled from the
name claim of the identity the sign-in resolved — and its shipped ladder has two steps:
name is the only claim a slot is filled from as the product
ships today: a slot of the same source under another name gets no value, so a step that uses it is
always left out. Word the message per channel under
post-login-welcome:BySurface:in-channel:ByChannel:{channel}:Templates.
The levels of the two settings differ. The contract resolves from Tenant,
Application and Core; the template from those plus a ui_config record. So a record selected
by a request parameter can pick which of your wordings a user sees, yet can neither widen the slot
list nor introduce an action type of its own.
The
Tenant level. No reader for the Tenant level is registered out of the box — the
integrator writes one. Without it the resolution goes to Application/Core.Application level is the client entry. Above the core the address is relative to the
record of the level, and the record of Application is the entry of the client in
Veriqa:OpenIddict:Clients: the contract of a message for one application sits at
Veriqa:OpenIddict:Clients:[n]:MessageTemplates:{kind}:…:Contract.
Action types of confirmations your server creates are message kinds of their own, and they are
declared only above the core — in the client entry, as MessageTemplates:{action type} with a
Contract and Templates. A kind visible only in the host section Veriqa:MessageTemplates is
refused by POST /api/transaction/confirmation with action_type_unknown. A complete example with
the request and the answer: Server-to-server confirmation API.
The device_type axis refines any step. Its value is the type of the device the transaction was
started on, as the initiator context states it — desktop, mobile,
tablet or unknown — and its group, ByDevice, is the innermost one: it stands under whichever
node the rest of the address names, the bare {kind} included, and needs no surface. A step that
names the device is asked immediately before the same step without it, so a wording without
ByDevice keeps answering for every device no wording was stated for. Only the receipts are asked
about the device today; a transaction with no initiator context — it is switched off, or the device
type is not collected — is asked about without it. The contract of a message does not narrow by the
device.
The channel axis refines the surface, so it never appears without
surface. The product ships six values along it — both wordings of the pre-filled message, under
deeplink-prefill:BySurface:in-channel:ByChannel:whatsapp and …:ByChannel:email, and the request
for the phone number in Instagram and in Messenger, under
phone-number-request:BySurface:in-channel:ByChannel:instagram and …:ByChannel:messenger and at
the same two addresses of phone-number-request-required;
the other kinds state no value along this axis, and a step without a channel answers for every channel at once.
Asking a key about an axis it does not declare is an error.
Wording a message for one channel therefore takes the surface as well. The request for the phone
number is the plain case: phone-number-request ships Share your phone number at the bare {kind}
and a wording for each of two channels — Instagram and Messenger, which adds what to do without a
number in the profile (phone number) — and it is asked about with the surface in-channel
and the channel of the sign-in. The request for a Required number is the kind
phone-number-request-required, shipped and addressed the same way. A wording for one channel sits at
Veriqa:MessageTemplates:phone-number-request:BySurface:in-channel:ByChannel:{channel}:Templates:
appsettings.json
phone-number-request:Contract; the request fills the server slots app,
browser, os and region from the transaction, by the rules described for the receipts above, and
never a caller slot. End the ladder with a step that uses no slot: browser, os and region may
have no value, a caller slot of this kind never has one, and marking a slot Guaranteed or Required
does not change that. The
text is rendered as plain text and translated into the user’s language like any other wording.
Health endpoints
Readiness registers a check per configured dependency, so it reflects exactly what you wired up:
no database configured means no database check. The remaining endpoints, the metrics and the
traces: Observability.
Keys in the declaration catalog
The table below is built from the declaration catalog of keys: the name, the value type, the levels, the dimensions and the address on every level. The remaining keys are described in the sections above. The keys below are the ones Veriqa owns. The same catalog is open to what you ship: an extension of your own declares its keys on the core, application andui_config levels through the
same public API, and its declarations join this schema at startup — see
Declare your own configuration keys.
The “Default” column prints what the owner of the key declared: what the core level answers when its
record states nothing. For a key that declares no such answer, the column says so outright — its defaults live in
the sections above. “Declared, value not printed” stands where an answer is declared and has no text
to it: the declared answer is null, or the key is declared secret — the schema of a secret key
carries the fact of the answer and never its text. What an installation put into a setting is in no
column of this table: what is printed here is a declaration, not a resolution.
Next steps
Channel setup
Telegram, WhatsApp, other channels and Email — end to end.
Preparing for production
What to check before shipping.