HTTP: administration (reference)
The administration API under /admin/v1/ offers the administration store as OData sets and commands; the
administration app for Access and the VBA class WklGatewayClient are built on it.
Syntax
With an administration store, the service offers
its tables under /admin/v1/: as OData sets that an administration program links like tables—Access
via this service's ODBC driver, with the forms it already has for tables—and as commands for
anything that isn't a change to a row.
| Form | Address | Purpose |
|---|---|---|
| OData | GET /admin/v1/odata/, $metadata, {Menge}, {Menge}({Schlüssel}), {Menge}/$count | reading with $select, $filter, $orderby, $top, $skip, $count |
| OData | POST /admin/v1/odata/{Menge}, PATCH and DELETE on {Menge}({Schlüssel}) | create (201), update (200, If-Match with the version read), delete (204) |
| Command | POST /admin/v1/issue-key | issue a personal key: once in the body, as a one-time link or as a one-time link by email |
| Command | POST /admin/v1/activate-key | activate an on-demand key for a period (id, hours) |
| Command | POST /admin/v1/deactivate-key | end the activation of an on-demand key immediately (id) |
| Command | POST /admin/v1/save-sign-in-rule | create a sign-in rule with all networks, people and groups in one change, or replace it as a whole (id) |
| Command | POST /admin/v1/explain-sign-in | what the sign-in policy says for a person, per sign-in method and origin, with the triggering rules |
| Command | POST /admin/v1/sign-in-profile | what is usual for a person when signing in, over a chosen period (sign-in profile) |
| Command | POST /admin/v1/test-connection | open a data source; response: reachable, duration, version, database, login or the server's message with the error number and kind of failure (connection test and service account) |
| Command | GET /admin/v1/service-account | the account under which the service signs in to databases with Windows authentication (connection test and service account) |
| Command | POST /admin/v1/reload-catalog | discard the resource catalog of one (dataSource) or all data sources; the next request reads it again |
| Command | GET /admin/v1/check-store | check the store as a reload would, without changing anything |
| Command | POST /admin/v1/backup-store | one checked copy each of the store and its log file into the sicherungen folder next to it, named after the time (gateway-YYYYMMDD-hhmmss.db and gateway-YYYYMMDD-hhmmss-logs.db); response file, schemaVersion, bytes, logFile, logBytes (the last two empty without a log file). The service chooses the location; a request doesn't name a path. Two backups in the same second: the second gets 409. The copies contain no master key |
| Command | POST /admin/v1/reseal-store | like the command of the same name on the computer |
| Command | POST /admin/v1/enroll-totp | set up a person's second factor (person, delivery: key, link or mail); ends their sessions, with mail only on pickup. The one-time link page shows the address for the authenticator app as a QR code (SVG in the page, nothing is loaded externally) and next to it the key for manual entry |
| Command | POST /admin/v1/reset-totp | remove a person's second factor and end their sessions |
| Command | POST /admin/v1/set-geoip-source | choose the source of the geolocation database |
| Command | POST /admin/v1/update-geoip | download the geolocation database from the chosen source now (the server needs internet access) |
| Command | POST /admin/v1/upload-geoip | accept a geolocation database file brought by the caller; body = file |
| Command | POST /admin/v1/export-load | the load log as a file for the manufacturer, without persons, origins, or values |
| Command | GET /admin/v1/update-databases | databases of the update module with their settings, plus configurations left out |
| Command | POST /admin/v1/update-status | state of one database: open packages with their deadline, blocking and unknown sessions, drift (database updates) |
| Command | POST /admin/v1/update-history | finished packages of one database with their steps, newest first |
| Command | POST /admin/v1/confirm-update, dismiss-update, retry-update | confirm a waiting package; dismiss a failed or refused one, or apply it again |
| Command | POST /admin/v1/move-update-deadline, cancel-update-notice | move the deadline of an announced package; cancel the announcement |
| Command | POST /admin/v1/upload-update | put a package into a database's inbox and accept it right away; body = file |
| Page | GET/POST /admin/v1/pickup/{Kennung} | one-time link; without sign-in, the ID is the proof |
Parameters
Why two forms. Whatever is a list is a set: an administration program needs no code of its own for it. Whatever returns a secret or has an effect beyond the row is a command: a set that Access reads again after creating a row would either have to hand out the key just issued once more, or lose it.
Ready-made clients. The “Overview of WKL Database Gateway Manager and the VBA class” (documentation of the administration app) covers all sets and commands (sign-in with a key or Windows account and a second factor); “WklGatewayClient: VBA class for the data service” (documentation of the administration application) calls the commands from your own VBA applications.
Sets
Sets. They follow the store's tables one to one; lists belonging to a rule or a key are rows of a subset with the key of the parent row.
| Set | Key | Remark |
|---|---|---|
DataSources | Name | Name follows the naming rule for the alias. The connection comes either as the string Connection—write-only, always reads as empty—or as connection properties ConnectionProperties, from which the provider builds the string; one of the two on create, on update a value replaces the sealed string, empty leaves it in place. Provider, ProviderVersion, and ProviderOptions (JSON object of texts) as in the configuration. Switches AllowPassThrough, AllowCheckedStatements (default true), AllowSqlLogins as in the configuration |
Providers, ProviderProperties | Id, PackageVersion; Provider, PackageVersion, Scope, Name, Language | Read-only: the installed provider packages and the properties each describes for its connection; see connection properties of a provider |
DataSourceTables | DataSource, Setting, Pattern | Setting is row_stamp or keyless_write; create and delete only |
ImpersonationDenyRules, ImpersonationDenyItems | Id; RuleId, Item, Value | Item: login, role, from, except_from |
ClientRules, ClientRuleItems | Id; RuleId, Item, Value | Item: database, resource, verb, object, except_object (rights per object; the check rejects an entry in the wrong form with 422). Holders counts who has the rule (people, Windows accounts and groups, SQL Server logins). RuleVersion (read-only) is the version that AuditEntries.AccessRule names; if it differs from the version in an entry, the rule has changed since |
WindowsAccounts, SqlLogins | Account, Client; Login, Client | assignments to rules |
Persons, PersonRules | Id; PersonId, Client | SqlLogin (the person's own SQL Server login, for Impersonate with a personal key), WindowsAccount (name or SID, resolved on write), Email (an address without a display name, suggestion for an invitation mail; otherwise 400), DefaultRole (default identity), SecondFactor (none, pending, active), LeftAt (when the person left the organization, set by the administration, also in advance). Read-only: LastSignInAt and LastSignInMethod, the last successful sign-in (method as in the sign-in log, such as personalKey, windows); the service writes it once a minute, it outlasts the retention of the sign-in log and doesn't change the person's version. For display: Status/StatusKind, SecondFactorText/SecondFactorKind, counts Rules, ValidKeys, OpenSessions, ExpiringKeys (valid keys that expire within 14 days); PersonRules.Person names the person. The service refuses DELETE of a person who has left traces (keys, sessions, sign-ins, load, or their own changes) with 422; disable them with Enabled = false (Retention and deletion) |
PersonalKeys, PersonalKeyItems | Id; KeyId, Item, Value | issued only with issue-key. Changeable: Label, ReadOnly, TotpExempt (only for a read-only key), ExpiresAt (not beyond the maximum lifetime from issue), OnDemand and Revoked—a revocation can't be lifted. ActiveUntil is set only by activate-key/deactivate-key. For display: Status (“gültig” (valid), “läuft in 5 Tagen ab” (expires in 5 days), “abgelaufen” (expired), “gesperrt” (revoked), “ruht” (dormant), “frei bis 15:30 UTC” (active until 15:30 UTC)), StatusKind, ExpiresInDays, CanRevoke, CanActivate |
SignInFileRules | Position | Read-only. Sign-in rules that are still in the configuration file (Gateway:Security:SignInRules). With an administration store they are not in force: they only served as the starting value when the store was created, and the service warns about them at startup. Fields: Position (n as in file:n), Scope, Method, Effect, Mode, Label, Conditions (the lists in use as text, such as ExceptFrom: 10.0.0.0/8; ExceptHours: Mon-Fri 06:00-20:00), Exceptions (count), RuleVersion, InForce (always false with a store). To clean up, remove the entries from the file |
SignInRules, SignInRuleItems | Id; RuleId, Item, Value | sign-in policy; Item: from, except_from, person, except_person (value: Id of the person), windows_group, except_windows_group (name or SID), country, except_country (country code), operator, except_operator (network operator, AS3320), client, except_client (kind of program), hours, except_hours (time window). A rule's RuleVersion (read-only) is the version sign-in events and explain-sign-in name; if it differs from the version in an event, the rule has changed since. A rule's Mode: Enforce (default) or Report (Try a rule). The check rejects a person who doesn't exist or an invalid country code, network operator or time window with 422 |
SignInRuleExceptions, SignInRuleExceptionItems | Id; ExceptionId, Item, Value | exceptions with an end of a rule: RuleId, ValidUntil (UTC, empty = no end), Label, LapseLogged (read-only: the end is in the change log); Item: person (value: Id of the person), windows_group, country, from, operator, client, hours. The check rejects an exception without attributes and without an end with 422. Moving ValidUntil resets LapseLogged |
Settings | Name | totp_session_hours, self_service_key_days (self-service), totp_enrol_hours, max_activation_hours (on-demand keys), installation_name (takes precedence over Gateway:Store:TotpIssuer, see configuration), mail_link_hours (validity of a one-time link by email, 1–720 hours), audit_days (retention of the access log, 1–3650 days, takes precedence over Gateway:AuditLog:Days), log_max_megabytes (size of the log file, 64–65,536 MB, takes precedence over Gateway:Store:LogMaxMegabytes; applies without a restart, and the next trim removes the oldest entries first); any other name is rejected, an invalid value with 422 |
MailTemplates | Id | HTML templates for invitation mails, the same for all administrators: Name, Purpose (KeyPickup, TotpEnrollment, Welcome), Subject, Html; Builtin and ChangedAt are set by the service. Placeholders only from {{Person}}, {{Link}}, {{GueltigBis}}, {{Dienst}}, {{Absender}}, {{Organisation}}; KeyPickup and TotpEnrollment need {{Link}}; at most 256 KB. A violation: 400 with all reasons. The three built-in templates (one per purpose) can be replaced but not renamed (400) or deleted (422). A template embeds images as src="cid:brand" (the partner's logo) and src="cid:logo" (the mark of The White Knight Labs); the client attaches both with these IDs. The service doesn't send any mail |
SessionTokens | Id | second-factor sessions; only Revoked (final) and delete; Role names the bound identity. For display: Status (“offen” (open), “abgelaufen” (expired), “beendet” (ended)), StatusKind, CanEnd. EndReason names how a session ended: ended (ended in the administration), person-locked (person locked), key-revoked (the session's key revoked), factor-renewed (second factor set up again), factor-reset (second factor removed), expired (expired without being ended before); EndedBy names who ended it, as the change log names them (Konsole for the revoke-key command). The service ends sessions instead of deleting them; sessions ended before this version have both fields empty |
DatabaseIdentities, IdentityAssignments | Id; Id | database identities. Kind: Login, Own, Alias; the service checks a shared login on save with a trial switch. An assignment names exactly one source: PersonId, WindowsAccount or KeyId; the service adds Source, DataSource and Role |
DataSourceObjects | DataSource, Schema, Name | read-only. The objects in the catalog of every enabled data source, written the way a rule names them: Kind is the type under Resources (tables, views, functions, procedures), Pattern the entry under Objects (schema.name). Suggestions for an administration program; a data source whose catalog can't currently be read is missing from the list |
UpdateSettings | Database | Operating settings of database updates per database: Expand and Contract (automatic or confirm), NoticeMinutes (0–10080), SessionWindowMinutes (1–1440), the channels NotifyEventLog (on by default), NotifyFile and NotifyEmail (off by default, see reports) with the recipients NotifyEmailTo (mail addresses separated by ;; NotifyEmail without a recipient is refused with 400), backup retention BackupKeepCount (1–1000, default 10) and BackupKeepDays (0–3650, default 30); the service sets ChangedAt. A field left out gets its default; a value outside these ranges is refused with 422. Without an entry, and after it is deleted, the defaults apply |
SignInHours, SignInOrigins, SignInEvents, SignInFindings, SignInAdvice, GeoIpFiles, GeoIpSource | read-only; see Sign-in log and geolocation database | |
LoadShapes, LoadHours, LoadPersons, SlowRequests, LoadFindings, LoadAdvice | read-only; see Load log | |
ChangeLog | Id | read-only |
AuditEntries | Sequence | read-only. The audit stream (`Gateway:AuditLog`): accesses (Kind = Zugriff: time, caller, alias, resource, method, result, status, correlation) and custom statements (Anweisung, with text). With an administration store, read from the log file, also after a restart, for Gateway:AuditLog:Days; Sequence is then the number in the file. Per query, the set returns at most the newest 20,000 entries that match the period (At), the person (Person eq) and, with Reason ne null, “refusals only” of the filter. Without a store, the last 10,000 entries since start, with statements truncated to 4,000 characters. Person and KeyId name the person and personal key of a call, so that you can filter by them. For a denial by a sign-in rule, SignInRule names that rule's identifier and version (2@8da4235bc439aac0, see Grounds of the decision). Target names what the request addressed: path and query, readable (/odata/lager/dbo/Artikel(17), /odata/lager/dbo/Artikel?$filter=Nr eq 17), at most 2,000 characters, without $skiptoken; a query sent in the request body isn't included. For an answered request to a table or view, AccessRule names the access rule that allowed it: the rule's name and version (lager-lesen@3f2a8c41d07b9e65, the RuleVersion from ClientRules); the entry itself names the alias. Entries from before this extension have both fields empty |
ServiceStatus | Id | read-only, one row: load of the service since startup, from memory—RequestsLastMinute, RequestsLastFiveMinutes, InFlight, MedianMilliseconds, P95Milliseconds, DeniedLastFiveMinutes (401/403/429; without the answer to a request without credentials, with which every Windows sign-in starts), ErrorsLastFiveMinutes (≥ 500), Saturated (more than 50,000 requests in five minutes: numbers are a lower bound), the request limit (RateLimitPerCaller, RateLimitWindowSeconds, empty without a limit) and from the store OpenSessions, ValidKeys, ExpiringKeys (14 days). How far back the logs reach: AuditDays (retention of the access log in days, Gateway:AuditLog:Days; empty without a store or with the log switched off), AuditOldestAt (oldest access kept, empty without an entry), SignInEventDays (Gateway:SignInLog:EventDays), and LogMaxMegabytes (maximum size of the log file; empty without a store). Without the probes under /health |
Display fields. Status is a text for display, StatusKind one of five kinds (ok, warn, bad,
info, neutral) by which an administration program colors the label—the text is always shown with
it, never the color alone. Can… fields say whether a row command is currently possible. The service
computes them on read according to the store's clock; they're read-only (writing gives 400) and aren't
part of the row version—a key doesn't change just because a day passes. They're delivered rather than
computed in Access, because an expression such as IIf(…) in an Access query isn't translated through the
ODBC driver, and a data grid in server mode would then fall back to slow reading through Access.
Every set has the property Version (eight bytes of a SHA-256 over the stored values of the row,
including sealed ones) as its row version. If you change a row that someone else has changed since you
read it, you get 412—in Access, the familiar write conflict message. Values a set shows from another
set aren't part of the row version: the number Rules of a person, Holders of an access rule, the name
Person of an assignment, a key or a session. A new rule for a person or the person's new name therefore
doesn't turn an edit that's open at the time into a write conflict. Numbers are int (Edm.Int32),
not bigint: without its option for large numbers, Access shows bigint as text and then rejects a
condition such as Id = 3. The filter compares text case-insensitively, as SQL Server does
and as the store compares names.
Counting, highest value and grouping
The administration interface reads rows and counts them; it doesn't group. It rejects $apply with
400, and its schema document announces no grouping (Aggregation.ApplySupported is absent). The
ODBC driver uses that announcement to decide what it sends to the administration interface (the
translation in detail:
“Supported SQL (reference)” in the documentation of the ODBC driver):
| Statement Access passes to the driver | Request to the administration interface | Result |
|---|---|---|
SELECT COUNT(*) FROM "Set", for example from DCount("*", "Set") | Set/$count | Number of rows |
SELECT COUNT(*) FROM "Set" WHERE …, for example from DCount("*", "Set", "Id > 4711") | Set/$count?$filter=… | Number of matching rows |
SELECT … FROM "Set" WHERE … ORDER BY … | Set?$select=…&$filter=…&$orderby=… | Rows |
MAX, MIN, SUM, AVG, COUNT(column), GROUP BY, HAVING, DISTINCT, for example from DMax, DMin, DSum, DAvg, DCount("column", …) or a totals query | None | The driver rejects the statement with 42000 (“Der Dienst kündigt die Gruppierung über $apply … nicht an”: the service doesn't announce grouping); Access reports “ODBC–call failed” (3146) |
To count, use DCount("*", …), not DCount("column", …): only COUNT(*) becomes a count
request, with or without a condition.
To get the highest value of a column, read the first row of a query sorted in descending order.
Over HTTP, for example through an administration program's own HTTP access:
GET /admin/v1/odata/SignInFindings?$select=Id&$orderby=Id desc&$top=1. For SignInFindings you rarely
need it: to count only what's new, keep the highest Id you've read and request
SignInFindings/$count?$filter=Id gt 4711 (see Findings of the sign-in rules).
Check and change log
Every change is checked before it's applied. A change is a transaction: compare the version, write,
record in the change log, and then check the entire content the way a reload checks it—including the
rules a person names. If it doesn't pass, everything is rolled back, and the 422 response names every
error. Without this check, a misspelled rule would sit in the store, every reload would reject it, and the
service would silently stay on the old version, even though the API had reported success. An applied
change takes effect immediately (the service reloads without waiting for PollSeconds). The service also
answers a constraint of the store itself (unknown Item, duplicate key, sub-entry without a parent row)
with 422 and SQLite's message.
Change log. In the same transaction as the change: time, caller (Person (Kennung) (person (ID)),
service name, Windows account or Einrichtung (setup)), operation (insert, update, delete,
command), set or command, key, values before and after as JSON. A connection string and connection
properties appear only as “neu gesetzt” (newly set), a key never. A change that was rolled back leaves no entry; an applied one
always has one. Sub-entries deleted together with their parent row aren't listed individually.
Right and first administrator
Right. A rule must name the resource admin literally; * isn't enough, as with the sql right.
The rule's Databases don't restrict the administration—it belongs to no data source. A rule for the
administration:
{ "Id": "verwaltung", "Resources": [ "admin" ], "Verbs": [ "*" ] }
With "ReadOnly": true, the same rule reads the administration and tests connections but changes nothing
(for example for an audit):
{ "Id": "revision", "Resources": [ "admin" ], "Verbs": [ "*" ], "ReadOnly": true }
You can sign in to the administration with a personal key and a Windows account, with a service key only
if Gateway:Store:RequireAdminTotp = false and no sign-in rule requires a second factor for the
administration—a new store contains such a rule (otherwise 403 “… gehört zu keiner Person” (… doesn't
belong to any person), because a service key can't have a second factor), and not with a SQL Server login:
SQL Server checks its password at a data source, and the administration belongs to none. A personal key
that is restricted to data sources never reaches the administration; a read-only key can at most read it.
Every address of the API responds only over a protected connection: HTTPS, a trusted proxy with
X-Forwarded-Proto: https, or a request from the same computer. Otherwise 403, also for the one-time
link.
First administrator. Only someone named by a rule with admin reaches the administration. The first
such person is created by the init-admin command on the server, not by a request over the network, see
First administrator and recovery.
One-time links
One-time link. issue-key with "delivery": "link" returns an address instead of the key, valid for
Gateway:Store:PickupMinutes (default 15 minutes, see configuration).
When opened, the page behind it shows only a button; only that button's POST shows the key, once. This
way, an email client or chat app that opens links for a preview doesn't pick up the key beforehand. The waiting
key is held only in the process's memory, never on disk; a restart invalidates open links. The pickup is
recorded in the change log (Einmal-Link (one-time link), with origin).
<a id=“one-time-link-by-email”></a>One-time link by email. issue-key and enroll-totp with
"delivery": "mail" return an address for an invitation mail (person, delivery, link,
linkExpiresAt), valid for mail_link_hours or MailLinkHours (default 72 hours). Unlike an ordinary
one-time link:
- No secret is waiting. The store holds the request and the SHA-256 checksum of the link, not a key. The key or second factor is created only on pickup; until then, an existing factor of the person stays unchanged. A copy of the store file therefore opens nothing.
- Survives a restart of the service.
- Exactly once. Opening it a second time shows “bereits abgeholt am … um …” (already picked up on … at …) (
410) with a request to report this to an administrator if the recipient didn't do it themselves; an expired or unknown link gives404. - Response without a key ID: the key exists only after pickup; it then appears in
PersonalKeys. The service rejects anexpiresAtbefore the end of the link with400. - Log: issuing as a command with
delivery: mail, pickup asissue-keyorenroll-totpwith the actorEinmal-Link von <Aussteller>(one-time link from <issuer>) and the origin. - If the request can no longer be fulfilled on pickup (person removed), the page shows this with
409; the link then remains unused.
{ "person": "Jane Doe", "label": "Excel report", "expiresAt": "2027-03-31T00:00:00Z",
"readOnly": true, "databases": [ "fachdaten" ], "from": [ "10.0.0.0/8" ], "exceptFrom": [],
"delivery": "key" }
Responses
For all sets and commands of the administration:
| Response | Meaning |
|---|---|
401 “Es wurde keine gültige Anmeldung vorgelegt.” (No valid sign-in was presented.) | no key or an unknown key |
403 “Die Verwaltung ist nur über HTTPS oder vom eigenen Rechner erreichbar.” (The administration can only be reached via HTTPS or from the same computer.) | unprotected connection |
403 “Die Verwaltung ist für diesen Aufrufer nicht freigegeben.” (The administration isn't enabled for this caller.) | no rule with admin; read-only rule on a change; key restricted to data sources |
400 | unknown field, missing required field, set without this operation (create PersonalKeys, change ChangeLog), lifting a revocation, expiry too late |
412 | the row was changed since it was read |
422 | content invalid after the change, or a constraint of the store; the message names the reason |
Connection test and service account
test-connection. Body { "dataSource": "fachdaten" }. The response names dataSource,
reachable, elapsedMilliseconds, productVersion, database, and login; if the data source can't
be reached, also message (the server's message in its own language), number, and failure.
The test opens the connection once, without the connection pool, and reports the server's answer; the
database driver doesn't retry it. Only after 233 (“no process is on the other end of the pipe”) does
it open a second time at once: a SQL Server on the same computer sometimes reports a refused login
over Shared Memory or Named Pipes that way instead of with 18456. If the server answers 233 again, it
really can't be reached:
{ "dataSource": "fachdaten", "reachable": false, "elapsedMilliseconds": 7,
"message": "Login failed for user 'NT SERVICE\\Wkl.Data.Gateway'.", "number": 18456, "failure": "Login" }
number is the database's number (for SQL Server, the error number); empty if there is none.
failure classifies the failure independently of the server's language; a program uses it to
explain the cause and suggest the remedy:
failure | Meaning | SQL Server, examples | Remedy |
|---|---|---|---|
Login | Sign-in refused: unknown login, wrong password, locked, expired | 18456, 18452, 18470, 18486–18488 | Create or enable the login in SQL Server (database administration) |
Database | Login accepted, the database is missing or not accessible to it | 4060, 4064, 911 | Create the user in the database or check the database name |
Permission | Connected, a permission is missing | 229, 230, 262, 297, 300, 916 | Grant the permission in the database |
Network | Server not reached: name, port, service stopped, firewall, encryption refused | 2, 26, 53, 258, 10060, 10061, severity 20 or higher | Check name, port, and firewall |
Timeout | No answer in time | -2 | Server load, Connect Timeout |
Unreadable | The server answers but can't read the database | 605, 823, 824, 942, 945 | Check the drive, restart the database server |
Configuration | Settings unusable: string rejected, provider not usable | – | Correct the data source's connection or provider |
Other | Any other failure | – | Read message and number |
A provider built for an older version of the provider contract doesn't return number and
failure; they then stay empty.
service-account. No body. The account under which a data source with Windows authentication
(Integrated Security) signs in to the database—the name a database administrator creates the login
for:
{ "account": "NT SERVICE\\Wkl.Data.Gateway", "kind": "Virtual",
"networkAccount": "FIRMA\\SRV-DATEN$", "domain": "FIRMA" }
| Field | Content |
|---|---|
account | The service's account; this is how a SQL Server on the same machine sees it |
kind | Virtual (virtual service account NT SERVICE\…), NetworkService, LocalSystem, LocalService, Managed (group managed service account DOMAIN\name$), or User |
networkAccount | How a SQL Server on another machine sees it: for Virtual, NetworkService, and LocalSystem the computer account DOMAIN\MACHINE$, for a domain account that account itself. Empty if no other machine knows the account (machine without a domain, LocalService, local user account)—Windows authentication then works only against a SQL Server on the same machine |
domain | The machine's domain (NetBIOS name); empty without a domain |
What SQL Server needs for this account is described under Installation—notes.
On-demand keys, sign-in rules, explanation
On-demand keys. issue-key with "onDemand": true issues a key that is dormant until an
administrator activates it. activate-key with { "id": "k…", "hours": 4 } activates it; a new
activation replaces the running one. The service rejects (400, the message names the reason) a key that isn't an
on-demand key or is revoked or expired, a duration above max_activation_hours, and an activation beyond the
key's expiry date. deactivate-key with { "id": "k…" } ends it immediately. Both are recorded with the
actor in the change log; the response names activeUntil.
Sign-in rule in one step. A rule applies as soon as it's saved. Via the sets, the rule is created first
and its attributes afterward—in between, a deny applies to everyone, including the person saving it.
save-sign-in-rule writes the rule and its attributes in one change:
{ "scope": "All", "method": "Any", "effect": "Deny", "label": "außen nur Frank",
"exceptFrom": [ "10.0.0.0/8" ], "exceptPersons": [ "John Doe" ] }
Further lists: from, persons, windowsGroups, exceptWindowsGroups, countries, exceptCountries,
operators, exceptOperators, clients, exceptClients, hours, exceptHours (meaning under
sign-in policy). The service stores network operators as AS
with the number and time windows in full form (Mon-Fri becomes Mon,Tue,Wed,Thu,Fri 00:00-24:00). mode is
Enforce (default) or Report for a rule on trial.
Exceptions with an end go in exceptions, each with persons, windowsGroups, countries, from,
operators, clients, hours, until (UTC) and label:
{ "scope": "Data", "effect": "Deny", "label": "Germany only",
"exceptCountries": [ "DE" ], "exceptFrom": [ "10.0.0.0/8" ],
"exceptions": [ { "persons": [ "John Doe" ], "countries": [ "TH" ],
"until": "2026-11-02T00:00:00Z", "label": "vacation" } ] }
With "id", the command replaces the rule including all its attributes and exceptions; without it, it
creates a new one. Response { "id" }.
The service answers an unknown person or rule with 400, content that is invalid afterward with 422;
in both cases nothing is saved.
Explain sign-in. explain-sign-in uses the same policy as a real sign-in to calculate what applies to
a person—for each of the person's sign-in methods (Windows account, SQL Server login, each personal key)
and each origin given:
{ "person": "John Doe", "scope": "Data",
"origins": [ { "name": "Firmennetz", "address": "10.1.2.3" },
{ "name": "Außen", "address": "203.0.113.7" },
{ "name": "Thailand", "address": "203.0.113.7", "country": "TH" },
{ "name": "Unbekannt", "address": null } ] }
An origin may state its country itself with country; that way you can ask “what applies to John in
Thailand?” without knowing an address there. Without country, the service reads the country from the
geolocation database as it does for a real sign-in. Likewise, an origin takes operator (network operator,
AS3320; without it, from the geolocation database), client (kind of program; without it, unknown) and at
(point in time in UTC; without it, now), for example
{ "name": "Saturday with Excel", "address": "203.0.113.7", "client": "Power Query", "at": "2026-10-24T10:00:00Z" }.
For each sign-in method (ways), the response contains one cell per origin with result—allowed,
code (second factor required), denied or a state that applies before the policy: revoked, expired,
rests (on-demand key is dormant), personDisabled, outsideKeyNetworks—and the matching rules
(id, version, label, effect, method, scope); floor shows the lower bound of the administration, country the
country the verdict used (empty when it's unknown or no rule names countries), operator, client and at the other
signals judged, tried the rules on trial
that would apply (like rules; they don't go into result), and spared the rules an
exception lifts (id, version, rule, effect, exception, until). id and version are the
same values a sign-in event names (see Grounds of the decision), so you can
check whether a rule still stands as it did at an earlier sign-in. A Windows
account is assessed without its groups, because only a real sign-in knows them; if a rule names groups,
groupsUnknown is included. 1 to 16 origins per call.
Sign-in log and geolocation database
The sign-in log records every sign-in decision with its origin in the
log file next to the store (settings under
`Gateway:SignInLog`, procedure in
Check sign-in patterns and origins). The service writes the
collected decisions once a minute and applies the sign-in rules as it does, so a sign-in shows up in
the sets up to a minute later. All sets are read-only. SignInFindings reads stored findings; the
other sets are computed when read.
| Set | Key | Content |
|---|---|---|
SignInHours | Hour, Method, Outcome, Country, NetClass | count (Count) per hour (UTC, on the full hour), plus Weekday (1 = Monday) and HourOfDay by the server's clock. No person, no address; kept for HourDays days |
SignInRuleReports | Id | per rule on trial and version, over the retention of the events: Rule (identifier), RuleVersion, Label, Effect, Would (sign-ins it would have hit), Spared (sign-ins an exception would have lifted it for), Persons and Origins (number of distinct ones), FirstAt, LastAt. Counts only, without person or address, so readable without the right sign-in-events. Counted are sign-in events as the log keeps them: one successful sign-in per caller, method, and origin per SuccessWindowMinutes |
SignInOrigins | Id | per named network as a whole (NetName, Network empty), otherwise per subnet (Network, /24 or /48), with Country, CountryName, City, Latitude, Longitude, Asn, AsnOrganisation, the counts Successes, Refusals, Challenges, Persons, and FirstAt/LastAt |
SignInEvents | Id | the individual events, at most 20,000 of the matching ones per request, newest first. The service already reads conditions on Person, At, and Id from the log file, so the limit applies only to what matches. Fields: At, Repeats, Method, Scope (data, admin), Alias, Outcome, Reason, Status, Person, KeyId, Presented, Origin, Network, NetClass, NetName, location and network operator as above plus Region, Client, CorrelationId, Rules, Signals (see Grounds of the decision), Weekday, HourOfDay. Only with the sign-in-events right, otherwise 403 |
SignInFindings | Id | the stored findings of the sign-in rules, Id ascending and stable: Rule, RuleVersion, Level (act, ask), At, Person, KeyId, Observed, Usual, BaselineDays, Text, Advice, EventIds (the Id values of the underlying events, comma-separated). Person, KeyId, Usual, and EventIds only with the sign-in-events right, otherwise empty. At most 20,000 per request |
SignInAdvice | Id | notable patterns of the last seven days, computed when read, Id valid only within one request: Code, Text, Advice, FromAt, UntilAt, Count, Origin, Country; Person and KeyId only with the sign-in-events right. The text never names a person |
GeoIpFiles | Kind | per file (City, Asn): DatabaseType, BuildDate, AgeDays, Stale (older than StaleDays), Bytes, InstalledAt, Attribution. Empty without files |
GeoIpSource | Id | one row: Source, Account, HasLicenseKey, UpdateDays, LastAttemptAt, LastError, Attribution. The license key is never returned |
Values. Method: apiKey, personalKey, windows, sqlLogin, none. Outcome: success,
refused, challenge (a request without credentials or an intermediate step of a Windows sign-in;
also the prompt for the second factor), totpSuccess, totpFailure. NetClass: loopback,
private (a named network, RFC 1918, unique local IPv6 addresses, link-local, 100.64.0.0/10),
public, unknown. In SignInHours, Country is empty when no country is known. Reason (for
refused): notSignedIn, unknownKey, revoked, expired, rests, personDisabled,
outsideKeyNetworks, denied, deniedByCountry (every refusing rule names countries), noPerson,
enrolmentOpen, noFactor, secondFactorMissing,
sessionExpired, limit, unprotected (a sign-in accepted only over a protected connection arrived
unencrypted). Presented holds a submitted, unknown SQL Server login name—never a key. Client is the
kind of program from the User-Agent header (Power Query, WinHTTP, PowerShell, Browser, or the
first word); empty without the header.
Grounds of the decision
Every sign-in event the sign-in policy judged names, in Rules, the rules the decision was based on,
and in Signals the signals it was judged by—both as JSON text. An event without a valid first factor
(wrong key, wrong password) never reaches the policy; both fields are empty there.
Rules is a list; empty ([]) when no rule applied. Each entry names:
| Field | Meaning |
|---|---|
id | The rule's identifier: its Id in SignInRules, or file:n for the n-th rule of the configuration file—which occurs only without an administration store, so never in events the administration API reads. floor stands for the administration's lower bound (RequireAdminTotp), which is no rule |
version | A fingerprint of what the rule does: 16 hexadecimal digits that change as soon as the part, method, effect, a condition, or an exception with its end changes—not when only the note changes. Equal rules have the same version everywhere |
effect | deny or requireTotp |
label | The rule's note at the time of the sign-in |
spared | true when an exception lifted the rule for this sign-in; absent otherwise |
mode | report for a rule on trial: it decided nothing, the entry shows what it would have done; absent for an enforced rule |
exception | The note of the exception that lifted the rule |
Signals names what went into the decision beyond the event's columns:
| Field | Meaning | Reliability |
|---|---|---|
country | The country the rules judged by; absent when it was unknown | observed, derived from the address (VPNs and mobile networks shift it) |
countryAsked | Whether a rule names countries, so that the country was looked up | – |
secondFactor | valid (valid session shown), missing (demanded, not shown or expired), notRequired, exchange (the request opens a session or sets up the factor), notChecked (refused before) | verified |
totpExempt | Whether the personal key is exempt from the second factor | verified |
operator | Network operator the rules judged by (AS3320); missing when it was unknown or no rule names network operators | observed, derived from the address |
client | Kind of program the request stated; missing without a User-Agent | claimed |
trust | For each signal named, its level: observed, claimed or verified (for secondFactor) | – |
Method, person, key, and origin are in the event's columns (Method, Person, KeyId, Origin,
NetClass, NetName); Client is a claim of the program and decides only when a rule asks for the program. The log folds equal
denials of a minute only when the same rules in the same version decided them. To search, use
contains, for example $filter=contains(Rules,'"id":"7"') for every sign-in rule 7 took part in.
The sign-in-events right. A rule must name the resource literally, in addition to admin;
neither * nor admin alone grants it. The address and person behind each sign-in are personal data;
the right separates who sees counts and patterns from who can follow individual people. It applies to
SignInEvents, to the person fields of SignInFindings and SignInAdvice, and to
`sign-in-profile`. Without the right, these fields are empty before the request's
filter applies, so a condition on them matches nothing.
Findings of the sign-in rules
Each time it writes, the service applies the sign-in rules to the new events and stores whatever a
rule detects as a finding. A finding is written once and never re-evaluated; it names the rule with
its thresholds (RuleVersion) and the comparison period (BaselineDays) it was raised under. The
same situation—rule, origin or person, hour or day—yields only one finding.
Since the last look. Id rises with every finding and is never reused. A program remembers the
highest Id it has read and from then on asks only for what is new with $filter=Id gt 4711. With a
condition on Id, the service reads the oldest matching findings first, so a program reads even more
than 20,000 new findings completely, in steps. Without such a condition, it returns the newest.
Levels. act calls for action: an attack pattern or a key that was passed on. ask is worth a
question to the person: something is new or unusual for them. Where the service reports findings is
set by `Gateway:SignInLog:Alerts`.
| Rule | Level | Detected when |
|---|---|---|
failureWave | act | at least 20 refused sign-ins from one address within an hour |
spray | act | refusals for at least 5 different names or keys from one address within an hour |
distributed | act | refusals for the same name from at least 10 addresses within an hour |
impossibleTravel | act | two successful sign-ins of one person at least 500 km apart, faster than 1,000 km/h |
keyManyOrigins | act | one personal key used from at least 5 public networks in a day |
newCountry | ask | a country the person didn't sign in from in the previous 90 days |
newClient | ask | a kind of program the person didn't use in the previous 90 days |
newNetworkOperator | ask | a network operator the person didn't sign in through in the previous 90 days |
newMethod | ask | a sign-in method the person didn't use in the previous 90 days |
unusualHour | ask | an hour in which, and next to which, the person never signed in during the previous 90 days |
The rules about something new speak only once the person had at least 5 successful sign-ins in the
previous 90 days, unusualHour only from 20; before that, everything would be new. Days and hours
count in the time zone of Gateway:SignInLog:TimeZone. Observed is the value seen (a country, a
count, a distance); Usual holds the person's usual values for comparison. A finding's text never
names a person.
Sign-in profile
sign-in-profile shows what is usual for a person when signing in—for example, to put a newCountry
finding in context. Body:
{ "person": "Frank Mustermann", "days": 365 }
days counts back from today, today included, in the time zone of Gateway:SignInLog:TimeZone;
default 90, allowed 1 to 3,650. The response holds person, days, from, until, total
(successful sign-ins), and activeDays, and under dimensions, per characteristic—country,
network, operator, client, method, hour (hour 00 to 23)—the values with value,
count, firstDay, and lastDay, most frequent first. An excerpt:
{ "person": "Frank Mustermann", "days": 365, "from": "2025-10-21", "until": "2026-10-20",
"total": 1184, "activeDays": 213,
"dimensions": { "country": [ { "value": "DE", "count": 1170, "firstDay": "2025-10-21", "lastDay": "2026-10-20" },
{ "value": "IT", "count": 14, "firstDay": "2026-07-28", "lastDay": "2026-08-08" } ] } }
Only successful sign-ins count. The profile comes from daily values per person, which stay in the log
file longer than the individual events. It requires the sign-in-events right (otherwise 403) and
always covers exactly one person; there is no list across persons. Unknown person: 404; days
out of range: 400.
set-geoip-source. Body:
{ "source": "maxmind", "account": "123456", "licenseKey": "…", "updateDays": 7 }
source is required: none (files only through upload-geoip), dbip-lite, or maxmind.
updateDays 0 to 90: after that many days, the service downloads again on its own (0 = only on
command); after a failed attempt, it tries again after six hours. licenseKey is stored sealed with
the master key; if the body omits it, the stored key stays, and "removeLicenseKey": true removes it.
maxmind without account ID or license key: 422. The change log only records whether a key is set.
update-geoip. No body. Downloads both files and puts them into use without a restart. The
response is files, each with kind, databaseType, buildDate, bytes, changed, attribution;
a file with the same build date as the current one stays as it is (changed: false). Refused with
422 and the reason: no source chosen, the source refuses or delivers nothing, the file is unusable, or
a download is already running.
upload-geoip. The body is the file itself (.mmdb, .mmdb.gz, or MaxMind's .tar.gz),
Content-Type: application/octet-stream. Query options: source=dbip-lite|maxmind sets the
attribution; force=true also accepts a file that is older than or as old as the current one. The
service recognizes the kind (City, Asn) from the content. The response is like one entry of
update-geoip. Larger than GeoIp:MaxMegabytes: 429 (LIMIT_EXCEEDED), like any oversized body;
unusable or older than the current file: 422.
Geolocation database sources
| Source | Retrieval | License and attribution |
|---|---|---|
dbip-lite | DB-IP Lite, monthly, no account; the service downloads dbip-city-lite-YYYY-MM.mmdb.gz and dbip-asn-lite-YYYY-MM.mmdb.gz from download.db-ip.com (early in a month, the previous month if needed) | CC BY 4.0; attribution required: “IP Geolocation by DB-IP (https://db-ip.com)” |
maxmind | MaxMind GeoLite2 City and ASN, with the account ID and license key of a free MaxMind account | MaxMind's GeoLite2 license; notice “This product includes GeoLite2 data created by MaxMind” |
The only outbound traffic is the file download; a caller's address never leaves the server.
Load log
The load log records what each shape of request costs (settings under
`Gateway:LoadLog`, procedure in
Investigate what loads the service and the database). A shape is a
request without its values: path, alias, kind, object, compared fields, sort order, and window,
with every value written as ?. Requests that differ only in their values are one shape. The
service writes the collected figures once a minute into the
log file, so a request appears in the sets up to a
minute later. All sets are read-only. LoadFindings reads stored hints; the other sets are computed
when read.
| Set | Key | Content |
|---|---|---|
LoadShapes | Id | Per shape over the last seven days: Path, Alias, Kind, Resource, Text (the shape), Template (the generated statement without values), FirstAt, LastAt, Requests, TotalMs, AverageMs, P95Ms, MaxMs, DataMs, Rows, AverageRows, Bytes, Errors, Refused, Persons, Advice (codes from LoadAdvice, comma-separated) |
LoadHours | Hour, ShapeId | Per hour (UTC, truncated to the hour) and shape: Requests, TotalMs, DataMs, MaxMs, P95Ms, Rows, Bytes, Errors, Refused, plus Weekday (1 = Monday) and HourOfDay by the server's clock. No person; kept for HourDays days |
LoadPersons | Day, ShapeId, Person | Per day (UTC), shape, and caller: Requests, TotalMs, Rows. Only with the load-persons right, otherwise 403; kept for PersonDays days |
SlowRequests | Id | Single requests from SlowMilliseconds on, the newest 20,000: At, ShapeId, Ms, DataMs, Rows, Bytes, Status, CorrelationId; Person and KeyId only with the load-persons right, otherwise empty. Kept for SlowDays days |
LoadFindings | Id | The hints from LoadAdvice, stored once an hour, Id ascending and stable: At (when the hint was raised), Code, ShapeId, Alias, Resource, Text, Advice, Requests, and AverageDataMs (figures of the seven days it is based on). One hint per code and shape; a shape that stays expensive doesn't appear again. No person. At most 20,000 per request; Id gt as for the findings of the sign-in rules |
LoadAdvice | Id | Shapes of the last seven days with a known remedy, computed when read, Id valid only within one request: Code, ShapeId, Alias, Resource, Text, Advice, Requests, AverageDataMs. The text never names a person |
Values. Path: odata (OData surface), cf (own routes under /api/v1/cf/), checked
(checked statement), passthrough (statement with the sql right), admin (administration),
other (second factor, self-service, downloads). Kind: read, count, aggregate, call,
insert, update, delete, statement, describe (statement only described), service (request
that reached no data: metadata, a refusal, the administration). The service keeps a statement's text
without literals and comments; long value lists are folded into ?, …. Skipped rows ($skip)
appear in Text only as a class, never with their number: skip=small (below 1,000),
skip=medium (below 10,000), skip=large (10,000 and more).
Times and amounts. TotalMs is the whole time of the request in the service; DataMs is the
part from opening the connection to the data source through the last row read, including the switch
to the person's identity. The 95th percentile (P95Ms) is the upper bound of the time class it falls
into (classes 5, 10, 25, 50, 100, 250, 500, 1,000, 2,500, 5,000, 10,000, 30,000 ms; above that,
MaxMs). Bytes counts the response before compression. Errors counts 5xx responses, Refused
counts 4xx responses. Person is the person's name; without a person, it is the caller's
identifier as in the audit. Sums of time, rows, and bytes are floating-point numbers so that a
program such as Access, which reads integers as 32-bit, doesn't overflow.
Advice (LoadAdvice). Each code is a pattern that loads the database regardless of the data:
| Code | Detected when | Usual remedy |
|---|---|---|
wholeSet | Reading without a filter returns at least 1,000 rows per request on average | Set a filter, select only the columns you need |
unindexedFilter | Filter on a table with at least 10,000 rows (the catalog's estimate), and none of the compared columns leads an index | Have an index created with the first filter column, or filter on an indexed column |
deepPaging | $skip of 10,000 rows or more | Narrow with a filter, or read on through the next link |
countLarge | Total count ($count) with at least 500 ms in the data layer on average | Request the total only when needed |
aggregateWhole | Aggregates without a filter with at least 500 ms in the data layer on average | Narrow the set, or have the database precompute it |
expensiveStatement | Own statement with at least 1,000 ms in the data layer on average, or a 95th percentile of 2,500 ms or more | Check it in Query Store; store it as a procedure or indexed view |
refreshLoop | The same shape at least 600 times in one hour, from at most three callers | Check the automatic refresh |
The load-persons right. A rule must name the resource literally, in addition to admin;
neither * nor admin alone grants it. The load per person shows who worked how much and when; the
right separates who sees the service's load from who can compare people.
export-load. Optional body:
{ "days": 30 }
days ranges from 1 to HourDays; the default is 30. The response is a JSON file
wkl-load-YYYYMMDD-hhmmss.json in the format wkl-load-export/1 with shapes, hours, advice,
and the bounds of the time classes (histogramBounds). It contains the names of aliases, objects,
and columns, but no persons, keys, addresses, correlation IDs, or values; the daily figures per
person and the single slow requests aren't even read for it. The required right is read access to
the administration; the export is recorded in the change log.
Finding a shape in SQL Server
The service marks every statement it generates with the identifier of its shape as a comment
after the first keyword, for example SELECT /* wkl:5b9b83f10953 */ TOP (1) … or
UPDATE /* wkl:8b7d7b280081 */ [dbo].[Probe] SET …. In that position, the comment is also kept by
Query Store, which doesn't store a comment in front of or behind a statement. For a procedure call,
the identifier precedes the call; Query Store then records the procedure's statements without it. With this identifier, a database
administrator finds the shape in the plan cache, in Query Store, or in Extended Events and gets from
there to LoadShapes. Checked statements carry the identifier as well; a statement with the sql
right runs unchanged, without an identifier. The connections identify themselves to SQL Server as
Wkl.Data.Gateway (program_name) unless the data source's connection string sets an
Application Name of its own.
Database updates
The service's update module applies signed database updates (packages
<product>-schema-<version>.zip) to the databases for which the folder
`Gateway:Updates` holds a protected configuration. It checks every
PollSeconds seconds for packages in the inbox whose copy finished at least 5 seconds ago, and for
announced packages whose deadline has passed. The commands show the state and carry out the administrators' decisions.
Rights. update-databases, update-status, and update-history read (a rule with admin is enough, even a read-only one). Every decision needs the right to change and is written to the change log with the
database, the package, and the outcome.
update-databases. No body. The databases with a trusted configuration and their settings, plus
the files the module leaves out:
{
"folder": "C:\\ProgramData\\Wkl.Data.Gateway\\updates",
"databases": [ {
"id": "demo-prod", "gatewayAlias": "demo", "product": "Demo", "dialect": "sqlserver",
"connection": { "string": "Server=sql01;Database=Demo;User ID=wkl_update;Encrypt=True",
"secret": "file:C:\\ProgramData\\Wkl.Data.Gateway\\secrets\\demo-update.txt" },
"keys": [ "9c0f6d1e…" ],
"inbox": "D:\\Updates\\Demo", "archive": "D:\\Updates\\Demo-Archiv",
"backup": { "mode": "native", "target": "D:\\Sicherung" }, "starterSource": null,
"settings": { "expand": "automatic", "contract": "confirm", "noticeMinutes": 60, "sessionWindowMinutes": 15 }
} ],
"problems": [ { "file": "alt.json", "code": 9, "message": "…" } ]
}
| Field | Content |
|---|---|
id | Key of the database in every other command (database) |
connection.string | Connection for structure changes and backups, without a password |
connection.secret | Reference to the password (env:NAME or file:PATH); the value is never returned |
keys | Publisher keys as SHA-256 fingerprints |
settings | Operating settings: expand and contract (automatic or confirm), lead time of an announcement (noticeMinutes), time window in which a request through the service counts as an open session (sessionWindowMinutes). They are changed in the set `UpdateSettings` and apply from the next pass. If they cannot be read, the database waits instead of running on the defaults |
problems | Files left out, with code and reason; 9 means not trusted (users other than Administrators and SYSTEM can change it, a password in the connection, connection.secret missing, file name ≠ id) |
update-status. Body { "database": "demo-prod" }. Response: the status object of database
updates (format 2), the same as wkl-db status --json:
{
"format": 2, "id": "demo-prod", "product": "Demo", "schema": 13,
"packages": [ { "id": 41, "file": "Demo-schema-14.zip", "schema": 14, "kind": "contract",
"status": "announced", "reason": null, "message": null, "deadlineUtc": "2026-11-02T18:00:00Z" } ],
"sessions": [ { "product": "Demo", "version": "2.3.1", "path": "gateway", "host": "10.1.2.17", "user": "FIRMA\\mustermann", "lastSeenUtc": "2026-11-02T16:40:12Z" } ],
"drift": []
}
packages holds the packages that are neither applied (done) nor dismissed (dismissed):
waiting (waits for confirmation), announced (the announcement is running; deadlineUtc is the
deadline), running, failed, refused. reason is the code that explains the status (see below).
sessions lists sessions of older applications that hold an announced package back, and sessions that
name no product and version, which don't hold it back; the service identifies sessions that come through it by the header `X-Wkl-Application`.
drift lists objects changed outside a package since the last one, each with object (name with
schema), type (kind of object, such as table or view), and change (changed, missing;
dateChanged is only a hint for an encrypted module and holds no package back).
update-history. Body { "database": "demo-prod", "skip": 0, "take": 20 } (take 1 to 100,
default 20). Finished packages (done, failed, refused, dismissed), newest first, with id,
file, schema, kind, status, reason, message, receivedUtc, confirmedBy, confirmedUtc,
sha256, and steps: per step phase (predeploy, pre, model, post, postdeploy,
baseline), version, path, sha256, appliedUtc, appliedBy, durationMs, result (ok,
failed, rolledback), and message.
Decisions. Body { "database": "demo-prod", "package": 41 }; package is the number (id)
from the status or the history.
| Command | For status | Effect |
|---|---|---|
confirm-update | waiting | expand: runs at once. contract: is checked and announced (a notice to the applications, deadline after the package's lead time or noticeMinutes) and runs at the deadline as soon as no older application is open. The database records gateway:<name> (<id>) as the person who confirmed |
dismiss-update | failed, refused | Becomes dismissed and leaves the status; the file stays in the folder failed |
retry-update | failed, refused | Takes the unchanged file from failed again, with every check, as a new package; the old one becomes dismissed. A changed package needs a new file |
move-update-deadline | announced | Body also "deadlineUtc": "2026-11-03T06:00:00Z"; not in the past. Applications read the new deadline when they next poll |
cancel-update-notice | announced | Cancels the announcement; the package waits again (reason 7) and needs a new confirmation, also with contract = automatic |
upload-update. POST /admin/v1/upload-update?database=demo-prod&file=Demo-schema-14.zip, body =
the ZIP file, at most 64 MiB. The service puts it into the inbox and takes it at once, like a file
dropped there. The file name is a plain name without a path.
Response to a decision or an upload. message says what happened; package says what became of
the package when it ran or was announced (otherwise null):
{ "message": "Angekündigt bis 2026-11-02 18:00:00Z.",
"package": { "file": "Demo-schema-14.zip", "exitCode": 0, "outcome": "announced", "message": "Angekündigt bis 2026-11-02 18:00:00Z." } }
outcome is done (applied; the file is in the archive and in done), announced, waiting (still
waits, for example for confirmation or for sessions), or failed (refused or failed; the file is in
failed). exitCode and reason use the same codes:
| Code | Meaning |
|---|---|
0 | OK |
2 | Package invalid or not trusted (signature, checksum, publisher, product, kind) |
3 | Database doesn't fit: higher schema version, or objects of its own without a history |
4 | A script or the deployment plan failed; everything was rolled back |
5 | Backup failed |
6 | Sessions of older applications are still open |
7 | Waits for confirmation; the message names the reason |
10 | Another run is applying a package of this product right now |
11 | Drift: an object was changed outside a package |
12 | Migrations don't match the history (checksum, missing or out-of-order migration) |
13 | The plan could cause data loss |
| Response | Meaning |
|---|---|
404 | No database with this id, or no package with this number |
409 | The package has another status than the command needs; on retry, the file in failed is missing or has changed; on upload, the same package is already waiting |
400 | Required field missing, file name with a path, deadline in the past |
413 | File larger than 64 MiB |
503 | The module is applying a package right now (try again shortly), or the database can't be reached |
Many operations at once
There's no bulk route: neither OData $batch nor a command for multiple people or keys. Each person,
each assignment, each key is a separate request.
| Question | Behavior |
|---|---|
| How many requests in a row | The administration counts against the caller's rate limit like any other request: Gateway:Security:RateLimit:PerCaller (default 1200) per RateLimit:WindowSeconds (default 60), after a pause up to RateLimit:Burst (default 600) at once, see configuration. Above that, the service responds with 429 and says in Retry-After after how many seconds the next request passes. There's no separate limit just for the administration |
| Partial failures | Each request is its own transaction. If the 300th request fails, the 299 before it stay saved; nothing is rolled back. The caller continues with the failed one |
| Repeating after a connection drop | With the Idempotency-Key header here too: it takes effect for every method except GET and HEAD, so also when creating via OData and for the commands. Rules and retention (10 minutes, in memory only, 401/429/5xx not stored) are described in Repeated write requests |
| Repeating after a restart or after more than 10 minutes | Without a stored response. Check beforehand whether the record already exists: the service rejects a second person with the same name, but a second key would really be created |
Worked example: 847 people, each with create, rule assignment and key, come to roughly 2,500 requests, so
with the default the first 600 go at once and the rest at 20 a second, so a little over a minute and a half in total. If you need to be faster, raise PerCaller for the duration of
the setup.
The effort can often be avoided. Only those who get personal keys, a second factor or their own default
identity need to be people. Rights and sign-in rules can be assigned to a Windows group as a whole
(WindowsAccounts, IdentityAssignments, attribute windowsGroups of the sign-in rules), without entering
its members individually. A second factor, however, always belongs to a person: if a rule requires one for a
group, every member without a person entry is rejected with 403
(see Sign-in).
Members of a Windows group
The service doesn't deliver member lists. It never queries Active Directory: it takes a caller's groups
from their Windows sign-in, and it resolves names to SIDs only when saving. If you want to create all
members of a group as people, read them out yourself, at a workstation or in a script (for example
Get-ADGroupMember or net group /domain), and then create a person with WindowsAccount for each
member. The service doesn't pick up later joins and departures in the group on its own; rights granted
via the group itself, however, follow the membership without any maintenance.
See also
- “Administrator guide for WKL Database Gateway Manager”