[ANCHOR-1270]: Move client (wallet) config from file to database, with a REST management API and JWT-carried client attribution - #1992
Conversation
* add `ismigratefromfileonstartup` flag to clients config * add `db` as a new client config type
* add `client_name` field to webauth jwt and jwt service. * refactor client name resolution to happen once during sep-10 and sep-45 challenge validation. * this simplifies client attribution in other sep services (sep-6, sep-12, sep-24, sep-31) by directly reading `client_name` from the jwt. * add database tables and rest api endpoints for client configuration management. * this allows dynamic updates to client configurations without redeploying the anchor. * add a migration runner to import clients from file-based configuration into the database on startup.
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds database-backed client configuration management, including REST endpoints and a one-time import path from file/yaml/json configs, and propagates resolved client_name into auth JWTs so SEP services can attribute requests without repeated lookups.
Changes:
- Introduces
client_configDB schema + JPA entity/repo and a JDBC-backedClientServiceimplementation. - Adds
/clientsPUT/GET/DELETE endpoints and an optional startup import runner. - Stamps
client_nameinto SEP-10/SEP-45 JWTs and updates SEP services/tests to readclientNamefrom the token.
Reviewed changes
Copilot reviewed 35 out of 35 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| platform/src/test/kotlin/org/stellar/anchor/platform/service/ClientConfigServiceTest.kt | Unit tests for DB client config CRUD/upsert validation and error translation |
| platform/src/test/kotlin/org/stellar/anchor/platform/service/ClientConfigImportRunnerTest.kt | Tests import runner iterates items and continues on failures |
| platform/src/test/kotlin/org/stellar/anchor/platform/data/JdbcClientServiceTest.kt | Tests JDBC-backed ClientService mapping and lookup behavior |
| platform/src/test/kotlin/org/stellar/anchor/platform/config/PropertyClientsConfigTest.kt | Tests DB config type ignores value and new migrate flag default |
| platform/src/main/resources/db/migration/V32__client_config.sql | Flyway migration introducing client config tables and uniqueness constraints |
| platform/src/main/resources/config/anchor-config-default-values.yaml | Documents new clients.type: db option and migration flag usage |
| platform/src/main/java/org/stellar/anchor/platform/service/ClientConfigService.java | Implements upsert/get/list/delete against JDBC repo with validation |
| platform/src/main/java/org/stellar/anchor/platform/service/ClientConfigImportRunner.java | CommandLineRunner to import config-defined clients into DB |
| platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientService.java | JDBC implementation of core ClientService |
| platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientConfigRepo.java | JPA repository with domain/signingKey lookup queries |
| platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientConfig.java | JPA entity mapping for client config and element collections |
| platform/src/main/java/org/stellar/anchor/platform/controller/sep/Sep10Controller.java | Aligns controller exception signature with updated SEP-10 service |
| platform/src/main/java/org/stellar/anchor/platform/controller/platform/ClientConfigResponse.java | DTO for client config API responses |
| platform/src/main/java/org/stellar/anchor/platform/controller/platform/ClientConfigRequest.java | DTO for client config API requests |
| platform/src/main/java/org/stellar/anchor/platform/controller/platform/ClientConfigController.java | Adds /clients CRUD endpoints |
| platform/src/main/java/org/stellar/anchor/platform/config/PropertyClientsConfig.java | Adds DB config type and migrate-from-file flag |
| platform/src/main/java/org/stellar/anchor/platform/component/share/ClientsBeans.java | Switches ClientService bean to JDBC when clients.type=db |
| platform/src/main/java/org/stellar/anchor/platform/component/sep/SepBeans.java | Wires ClientFinder for SEP-45 and updates SEP service constructors |
| platform/src/main/java/org/stellar/anchor/platform/component/platform/PlatformServerBeans.java | Registers ClientConfigService + conditional import runner beans |
| essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/ClientConfigApiTests.kt | Integration tests for /clients API and uniqueness enforcement |
| core/src/test/kotlin/org/stellar/anchor/sep6/Sep6ServiceTest.kt | Updates tests to rely on token.clientName instead of ClientFinder |
| core/src/test/kotlin/org/stellar/anchor/sep45/Sep45ServiceTest.kt | Adds tests ensuring SEP-45 stamps client_name and propagates auth failures |
| core/src/test/kotlin/org/stellar/anchor/sep31/Sep31ServiceTest.kt | Refactors tests to read client name from token/context rather than lookups |
| core/src/test/kotlin/org/stellar/anchor/sep24/Sep24ServiceTest.kt | Updates SEP-24 tests to set clientName on WebAuthJwt |
| core/src/test/kotlin/org/stellar/anchor/sep12/Sep12ServiceTest.kt | Updates SEP-12 tests to pass clientName via token rather than ClientFinder |
| core/src/test/kotlin/org/stellar/anchor/sep10/Sep10ServiceTest.kt | Adds tests for stamping client_name during SEP-10 validation |
| core/src/main/java/org/stellar/anchor/sep6/Sep6Service.java | Uses token.getClientName() for attribution (removes ClientFinder dependency) |
| core/src/main/java/org/stellar/anchor/sep45/Sep45Service.java | Stamps client_name into SEP-45 JWT using ClientFinder |
| core/src/main/java/org/stellar/anchor/sep31/Sep31Service.java | Removes client-name resolution logic; reads from token/context |
| core/src/main/java/org/stellar/anchor/sep24/Sep24Service.java | Uses token.getClientName() for attribution (removes ClientFinder dependency) |
| core/src/main/java/org/stellar/anchor/sep12/Sep12Service.java | Uses token.getClientName(); removes ClientFinder + warning log path |
| core/src/main/java/org/stellar/anchor/sep10/Sep10Service.java | Stamps client_name into web auth JWT during generation; surfaces as SepException |
| core/src/main/java/org/stellar/anchor/config/ClientsConfig.java | Adds DB config type and isMigrateFromFileOnStartup() |
| core/src/main/java/org/stellar/anchor/auth/WebAuthJwt.java | Adds client_name claim parsing/storage |
| core/src/main/java/org/stellar/anchor/auth/JwtService.java | Encodes client_name claim when present |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
* refactor client config service to use objectprovider * update client service to use objectprovider * update platform server beans to use objectprovider * fix unique constraint error handling in client config service * add specific http/https callback url validation * update client config validation to clear items when switching to db * delete method-level cross-origin annotations from client config controller
* delete `clients.migrate-from-file-on-startup` config property * update client config import runner condition to `clients.type=db`
* add parsing of clients.value for db backend migration * update client service in sep10 config to be transient
* add endpoints to manage client destination accounts * add endpoints to manage client signing keys
* add methods to manage client destination accounts * add methods to manage client signing keys * update client config data fetching with eager loading
* refactor clients.value resource loading for robustness * update behavior to log warning instead of failing on invalid resource paths
* add functions to manage client signing keys * add functions to manage client destination accounts * refactor client config parsing for legacy value formats when type is db
* add new `clients-db` configuration profile to use a database for client management * add extended tests for `clients-db` configuration, verifying sep-10 client attribution * update github actions workflow to run `clients-db` extended tests
* add new api endpoints for listing custodial clients * add new api endpoints for listing non-custodial clients * add service methods to list clients by their type
* add tests for client creation validation * add tests for managing client signing keys * add tests for managing client destination accounts * add tests for listing clients by type (custodial, noncustodial)
* refactor client database import instructions * delete 'migrate_from_file_on_startup' documentation * update client configuration to clarify direct import via 'value' field
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 42 out of 42 changed files in this pull request and generated 2 comments.
Suppressed comments (6)
platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientService.java:54
- These lookups convert any matching row to the requested subtype without checking its declared type. The API and legacy
RawClientformat permit irrelevant fields, so a noncustodial row with a signing key can be returned as aCustodialClient(and a custodial row with a domain asNonCustodialClient), unlikeDefaultClientService, which searches only the corresponding typed list. Filter by type before mapping (and ideally in the query).
platform/src/main/java/org/stellar/anchor/platform/controller/platform/ClientConfigController.java:57 - This static route conflicts with
/clients/{name}. Since validation permits any nonblank name, a client namedcustodialcan be created via PUT but GET resolves to this filtered-list endpoint instead of returning that client;non-custodialhas the same problem. Use a non-conflicting filter shape (for example a query parameter or/clients/type/...) or reject these reserved names consistently.
value = "/clients/custodial",
platform/src/main/resources/config/anchor-config-default-values.yaml:660
- This documentation says the import happens once and existing rows remain untouched, but the conditional runner executes on every startup and calls
upsert, which replaces matching rows and their collections. Operators following this text may lose API-managed edits after a restart. Document the actual repeated overwrite behavior.
platform/src/main/java/org/stellar/anchor/platform/service/ClientConfigService.java:22 - The incremental endpoints use detached read-modify-save cycles without optimistic locking or atomic collection-row operations. Two concurrent additions based on the same client state can both return success while the later merge overwrites the earlier addition, which defeats the purpose of safely managing high-volume collections incrementally. Add version-based conflict detection/retry or repository operations that insert/delete the targeted collection row atomically.
platform/src/main/java/org/stellar/anchor/platform/data/JdbcClientConfigRepo.java:30 - The eager graph makes every signing-key lookup materialize all domains, keys, and destination accounts.
SepRequestValidatorcalls this lookup on SEP-6/24 requests whose requested account differs from the JWT account, so the cited 600-account client causes hundreds of rows (potentially a cartesian product across all three collections) to be hydrated per request rather than limiting DB work to authentication. Add narrow attribution/name and destination-membership queries instead of using this full graph for every lookup.
@EntityGraph(attributePaths = {"signingKeys", "domains", "destinationAccounts"})
@Query("SELECT c FROM JdbcClientConfig c JOIN c.signingKeys k WHERE k = :signingKey")
JdbcClientConfig findBySigningKey(@Param("signingKey") String signingKey);
platform/src/main/java/org/stellar/anchor/platform/config/PropertyClientsConfig.java:135
- When both directly bound
clients.itemsand a migrationclients.valueare present, this assignment discards all pre-bound items. That contradicts the stated preservation behavior and can omit clients from startup import. Merge the parsed entries with the existing list using a deterministic name-conflict policy, or reject the combination explicitly.
items = parseLegacyValueForMigration(this.getValue());
* fix old jwt tokens by rejecting those without the client name claim * update jwt generation to always include the CLIENT_NAME claim, defaulting to empty string * fix client config parsing to prevent silent failures during migration * add tests for client config parsing error scenarios
Description
Raised by an anchor running the Anchor Platform: their partner-config YAML has grown to ~1,200 lines, and every change requires a rolling restart on their end. They asked whether config could move to the AP database instead — either AP checking the DB directly per request, or AP refreshing an in-memory cache every ~60 minutes — so onboarding a new partner (their stated goal is to automate onboarding as much as possible) or updating an existing one no longer needs a restart on their end.
Client (wallet) definitions — SEP-10 signing keys, SEP-24 domains, callback URLs, destination accounts — are configured today via
clients.type: file|inline|json|yaml, parsed once into an in-memory list at startup. Every lookup is a linear scan over that list, onboarding or revoking a single wallet means editing one shared YAML/JSON file, and any change requires a full restart. That breaks down past a few hundred clients: at 600+ entries the file becomes unwieldy to hand-edit safely, and there is no way to add or revoke a client without redeploying.Three designs were prototyped before this one: an in-memory cache refreshed on a timer/signal (the anchor's 60-minute-refresh suggestion; largest diff, up to an hour of staleness), a purely live DB lookup on every request (the anchor's other suggestion — AP checks the DB directly; simplest, but a DB round trip on every SEP-10/24/6/31/12 call), and this one — DB-backed live lookup, but only at SEP-10/SEP-45 auth time. This PR implements the third: SEP-10/SEP-45 resolve the client once via
ClientFinderagainst the database and stamp the result into a newclient_nameJWT claim; SEP-6/12/24/31 readtoken.getClientName()directly instead of each doing their own lookup. That removes the per-request database traffic the live-lookup-only design would add on every SEP-6/12/24/31 call, at the cost of client changes not taking effect for a session until its JWT expires (see Known limitations).A full REST API on the platform server lets anchors manage clients without a restart:
PUT/GET/DELETE /clients/{name},GET /clients(all),GET /clients/custodial/GET /clients/non-custodial(filtered by type), plus incrementalPOST/DELETE /clients/{name}/signing-keys/{key}and/destination-accounts/{account}. The incremental endpoints exist specifically for high-volume clients — one anchor's production config has a single custodial client with 600+ destination accounts — so adding or revoking one wallet doesn't require resending the entire list. Settingclients.type: dbwithclients.valuestill pointing at an existing file/yaml/json source auto-imports those clients into the database on every startup (upsert, not destructive), so an anchor can cut over without hand-transcribing every entry; switchingclients.typeaway fromdbafterward does not delete the imported rows.Changes
ClientsConfig.java: adds adbClientsConfigType. (No separate migration flag — see below.)WebAuthJwt.java: adds aclient_nameclaim field, populated from the decoded JWT.JwtService.encode: emits theCLIENT_NAMEclaim whentoken.getClientName()is set (the claim key already existed for the SEP-24 interactive/more-info JWTs; this reuses it for the SEP-10/45 web-auth JWT).Sep10Service.generateWebAuthJwt: resolvesclientFinder.getClientName(clientDomain, account)once, at token-issuance time, and stamps it onto the token before encoding.validateChallenge/generateWebAuthJwtnow declarethrows SepException(wasSepValidationException) so aSepNotAuthorizedExceptionfrom client resolution propagates and the login is refused, instead of silently issuing a token with no client name.Sep45Service.validate: same resolve-and-stamp for the SEP-45 contract-account flow; adds aClientFinderdependency.Sep6Service,Sep24Service,Sep12Service,Sep31Service: drop their ownClientFinder/ClientServicelookups; each now readstoken.getClientName()(orContext.get().getWebAuthJwt().getClientName()for SEP-31) directly, since SEP-10/45 already vetted it.V32__client_config.sql(new): createsclient_config,client_domain,client_signing_key,client_destination_account, with unique indexes ondomainandsigning_key— a hard constraint the file-based config never enforced.JdbcClientConfig.java(new): JPA entity for the tables above.JdbcClientConfigRepo.java(new): Spring Data repo with indexedfindByDomain/findBySigningKey/findByTypequeries,@EntityGraph-annotated sosigningKeys/domains/destinationAccountsare eagerly fetched (avoids aLazyInitializationExceptionwhen a config bean reads them outside a request-scoped Hibernate session).JdbcClientService.java(new):ClientServiceimplementation backed directly byJdbcClientConfigRepo— every lookup is a live, indexed query, no in-memory copy.PropertyClientsConfig.java:type: dbauto-detects and parsesclients.value(file path, inline YAML, or inline JSON) intoitemsfor import, without clearing any items already bound directly; no separate migration flag.PropertySep10Config.java:clientServicefield markedtransient— it's a JDK dynamic proxy (inherent to any Spring Data repo/service, not specific to@Transactional), and Gson would otherwise try to reflectively serialize it during debug logging and fail on a JPMSInaccessibleObjectException.ClientConfigController.java,ClientConfigRequest.java,ClientConfigResponse.java(new):PUT/GET/GET all/DELETE /clients/{name},GET /clients/custodial,GET /clients/non-custodial, andPOST/DELETE /clients/{name}/signing-keys/{key}+/destination-accounts/{account}, behind the same auth as the rest of the Platform API (see Known limitations).ClientConfigService.java(new): validates and upserts/gets/lists/deletes clients, plus the incremental add/remove-signing-key and add/remove-destination-account operations (rejecting removal of a custodial client's last signing key); translates aDataIntegrityViolationException(duplicate domain/signing key) into a clean 400 instead of a raw SQL error.ClientConfigImportRunner.java(new): aCommandLineRunner, gated only onclients.type=db, that imports every client currently resolvable fromclients.valueinto the database on each startup (upsert — safe to run repeatedly), logging and skipping any individual client that fails to import rather than aborting startup.ClientsBeans.clientService: returnsJdbcClientServicewhenclients.type: db, otherwise the existingDefaultClientService.PlatformServerBeans.java: registersClientConfigServiceand the conditionalClientConfigImportRunner.SepBeans.java: dropsClientFinder/ClientServiceconstructor args from the SEP-6/12/24/31 beans; addssep45toClientFinder's enabled-SEPs list and wires it into theSep45Servicebean.Sep10Controller.validateChallenge: throws clause widened toSepException.TestProfileRunner.kt: fixes a test-harness bug where inlineclients.valuecontent (JSON/YAML, not a file path) was unconditionally treated as a classpath resource and crashed withRuntimeException("Resource ... not found"); now falls back to treating it as inline content with a warning log.anchor-config-default-values.yaml: documents the newtype: dboption and its auto-import behavior (corrected from an earlier draft of this doc that described a since-removedmigrate_from_file_on_startupflag).Sep10ServiceTest.kt,Sep45ServiceTest.kt: new cases assertingclient_nameis stamped onto the JWT, and that aClientFinderauthorization failure propagates instead of issuing a token with no client name.Sep12ServiceTest.kt,Sep24ServiceTest.kt,Sep31ServiceTest.kt,Sep6ServiceTest.kt: updated to set/readtoken.getClientName()directly instead of mockingClientFinder.JdbcClientServiceTest.kt(new, 8 tests): domain/signing-key/name resolution, domain-preferred-over-account-match precedence, custodial-vs-noncustodial filtering.PropertyClientsConfigTest.kt(new, 5 tests):type: dbwith no value, with a YAML/JSON string, with a file path, and with items already bound directly — none of the four clear or duplicate the item list.ClientConfigImportRunnerTest.kt(new, 3 tests): upserts every item, continues past a single item's import failure, no-ops on an empty list.ClientConfigServiceTest.kt(29 tests): full coverage of validation, upsert-conflict handling, list/get/delete, and the incremental signing-key/destination-account add/remove operations including the custodial-last-key protection and cross-client uniqueness conflicts.ClientConfigApiTests.kt(essential-tests, 19 tests): real-HTTP coverage against a live Postgres for the full/clientsREST surface — CRUD lifecycle, 404s, validation (missing signing keys, no domain on a noncustodial client, malformed callback URL, unknown type, domain/signing-key collisions across clients), the four incremental endpoints (additive without disturbing existing entries, 404 on an unknown sub-resource, cross-client conflict, last-signing-key protection), and the two type-filtered list endpoints.ClientsDbAttributionTests.kt+ClientsDbTestSuite.kt(new, extended-tests, 4 tests): the one thing essential-tests can't cover, since it only runs against the default profile'sclients.type: file. A newclients-dbservice-runner profile (clients.type: db, wired intosub_extended_tests.yml) boots a real server with the DB-backed path active, and this suite runs an actual SEP-10 challenge/validate against it, decodes the resulting JWT, and assertsclient_nameis attributed correctly on create, follows a signing key moved between clients, stays unset for an unregistered key, and disappears after the client is deleted.Acceptance Criteria
PUT /clients/{name}with a valid custodial or noncustodial payload creates the client and returns it; a secondPUTto the same name updates rather than duplicates.GET /clients/{name}andGET /clientsreflect what's stored;GET /clients/{unknown}returns 404.GET /clients/custodialandGET /clients/non-custodialreturn only clients of the matching type.PUTwith a signing key or domain already used by another client, a noncustodial client with no domain, a custodial client with no signing keys, a malformed callback URL, or an unrecognized type all return 400 without creating/mutating a row.POST /clients/{name}/signing-keys/{key}and/destination-accounts/{account}add without disturbing existing entries; the correspondingDELETEremoves only the targeted entry; both 404 on an unknown client or an entry that isn't present; adding a signing key already used by another client returns 400; removing a custodial client's last signing key returns 400.DELETE /clients/{name}removes the client and its domain/signing-key/destination-account rows; a subsequentGETreturns 404.clients.type: db, a SEP-10 or SEP-45 login for a signing key/domain that matches a client in the database stamps that client's name into theclient_nameJWT claim — verified end-to-end (real HTTP, real Postgres) viaClientsDbAttributionTests, not just at the unit level.sep10.client_attribution_required: true, a login for a signing key/domain not in the database (or not on the allow list) is rejected outright, not issued a token with a null client name.client_name, without making their own database call to resolve it.clients.type: dbandclients.valuepointing at an existing file/yaml/json source, every client there is upserted into the database on startup, and startup does not fail if one client fails to import.clients.type: file|inline|json|yamldeployments are unaffected —clients.type: dbis opt-in, and switching away from it afterward does not delete previously-imported/managed rows.Context
Origin ask (anchor operator, paraphrased): their ~1,200-line partner-config YAML requires a rolling restart on every change; they asked for either AP-checks-DB-directly or a ~60-minute AP-side cache refresh, with automating partner onboarding as the underlying goal. A separate concrete follow-up from the same anchor: one of their custodial clients has 600+ destination accounts, and they didn't want to resend the full list to add or remove a single one — that's what the incremental signing-key/destination-account endpoints are for.
Testing
./gradlew :core:test :platform:test(includesClientConfigServiceTest,JdbcClientServiceTest,PropertyClientsConfigTest,ClientConfigImportRunnerTest)./gradlew runEssentialTests(includes the expandedClientConfigApiTests, real HTTP against a live Postgres)TEST_PROFILE_NAME=clients-db ./gradlew startServersWithTestProfilethen./gradlew :extended-tests:test --tests org.stellar.anchor.platform.suite.ClientsDbTestSuite(real SEP-10 auth against a DB-backed server, wired intosub_extended_tests.ymlfor CI)PUT/GET/GET all/incremental-endpoint round trips against a running server, verified against a live Postgres, including a full clean-restart migration of an existing file-based config into the database.Documentation
anchor-config-default-values.yamlupdated with theclients.type: dboption, its auto-import behavior, and the two new incremental-endpoint / type-filtered-list capabilities.Known limitations
N/A
Migrating
clients.typetodbThis is the guide for anchors moving their client configuration (custodial and non-custodial
wallets, signing keys, domains, callback URLs, destination accounts) off a static
file/yaml/jsonconfig and onto the newdb-backed option, where clients are managed live through the/clientsREST API instead of a redeploy.dbclientsatdbWhy move to
dbWith
clients.typeset tofile,yaml, orjson, the client list is fixed at deploy time —onboarding or updating a wallet means editing the config and restarting the platform server. With
clients.type=db, the same client records live in the database and are managed throughPUT/GET/DELETEcalls on/clients, so onboarding a new wallet doesn't require a deploy.Prerequisites
h2.data.typemust bepostgresoraurora.The in-memory
h2default won't persist client rows across restarts, andsqlitedoesn'tsupport Flyway's foreign-key migrations.
data.flyway_enabled=true. On first startup against that database, Flywayapplies
V32__client_config.sql, which creates four tables:client_config,client_domain,client_signing_key, andclient_destination_account. No manual migration step is neededbeyond having Flyway on.
/clientsendpoints live on the platform serverand are protected the same way as the rest of the Platform API (
platform_api.auth.type:JWTor
API_KEY). If you can already call other Platform API endpoints (e.g.PATCH /transactions), you can call/clients.1. Point
clientsatdbclients.valuecan stay pointed at whatever file (or inline YAML/JSON string) you were alreadyusing with
type=file/yaml/json— the format is unchanged. Ondb, that value is no longerread by the running server; it's only used once, on startup, to seed the database.
2. Start the platform server once to import
On startup, if
clients.type=db, a one-timeCommandLineRunnerreads every client out ofclients.valueand upserts it into the database by name. Check the startup logs for:A per-client failure (e.g. a duplicate signing key) is logged and skipped — it doesn't stop the
other clients from importing or block startup.
3. Stop re-importing on every restart
This import isn't a one-shot migration flag — it runs on every startup where
clients.type=dbandclients.valueis non-empty, and it re-upserts every client from that fileeach time, overwriting whatever is currently in the database for those client names. Once you've
confirmed the import succeeded, clear the value so subsequent restarts don't clobber changes made
through the API afterward:
4. Verify the import
curl -H "<your platform auth header>" http://localhost:8085/clientsConfirm the count and names match what was in your old config file.
Managing clients going forward
PUT/clients/{name}GET/clients/{name}GET/clientsGET/clients/custodialGET/clients/non-custodialDELETE/clients/{name}POST/clients/{name}/signing-keys/{signingKey}DELETE/clients/{name}/signing-keys/{signingKey}POST/clients/{name}/destination-accounts/{account}DELETE/clients/{name}/destination-accounts/{account}PUTexample (custodial client):Validation rules and constraints
domain. Requests violating this are rejected with
400.client_domain.domainandclient_signing_key.signing_keyare both unique across all clients. Reusing one on a differentclient returns
400.DELETE /clients/{name}/signing-keys/{signingKey}— that also returns400.http/httpsURLs.Rolling back
Deleting client rows isn't required to roll back. Setting
clients.typeback tofile,yaml,or
json(withvaluepointed at your old config) reverts to reading clients from that source;the database rows are simply unused while
typeisn'tdb.Troubleshooting
Startup log shows fewer imported than expected, with no per-client error. Check for a
duplicate signing key or domain across clients in your source file — the import calls the same
uniqueness-checked upsert the API uses, so a collision there fails the same way a
PUTwould.Clients you edited through the API reverted after a restart.
clients.valueis still set —see Stop re-importing on every restart.
/clientsreturns404 Not Foundon a client you're sure exists. Client lookups are byname and are case-sensitive; confirm the exact
nameused in the originalPUT.