Issue Description
stalwart-cli’s apply subcommand expects UPDATE operations with id as a top-level field on the op object, but the official documentation at Declarative bulk operations | Stalwart (and the Ansible/Terraform/NixOS/Pulumi integration examples on the same page) shows id as a dict key inside value. Plans copy-pasted from the docs fail with a misleading “X requires an id” client-side error that gives no hint about where id actually belongs, costing operators significant debug time. Two related concerns: the docs are wrong, AND the CLI’s error message is opaque.
Expected Behavior
Either:
- The documentation at Declarative bulk operations | Stalwart should show the actual accepted UPDATE shape (
idat the same level asobjectandvalue), OR - The CLI should accept the docs-shape (
idas a dict key insidevalue) as a deprecation alias.
Either way, the error message when an UPDATE op is missing the top-level id should be specific — instead of just “Domain requires an id” (which reads as “your value dict doesn’t contain an id field” rather than “your update op is missing the top-level id field”).
Actual Behavior
Following the docs verbatim:
{"@type":"update","object":"Domain","value":{"#dom-1":{"description":"updated"}}}
produces this client-side error:
error: Domain: update failed for id <empty>: error: invalidPatch ... Domain requires an id
The error is raised in crates/cli/src/commands/apply.rs:232 during Plan::resolve, before any HTTP request leaves the CLI. Server logs show nothing (we verified with RUST_LOG=trace on both client and server). Operators tune the wrong things (TLS config, schema migrations, recovery-mode init paths, postgres vs rocksdb) because the error doesn't say it's client-side.
The accepted shape is:
{"@type":"update","object":"Domain","id":"#dom-1","value":{"description":"updated"}}
The CLI's RawOp::Update enum deserialises id as a top-level field next to object and value; the docs do not reflect this.
### Reproduction Steps
Install stalwart-cli (1.0.5 from the official shell installer).
Run a Stalwart server (any backend — we observed this with both RocksDB and PostgreSQL, server 0.16.4).
Apply this NDJSON plan, exactly as the docs would suggest:
{"@type":"create","object":"Domain","value":{"dom-1":{"name":"example.com"}}}
{"@type":"update","object":"Domain","value":{"#dom-1":{"description":"updated"}}}
Observe the client-side error "Domain requires an id" with NO request reaching the server.
Confirm the correct shape works:
{"@type":"update","object":"Domain","id":"#dom-1","value":{"description":"updated"}}
### Relevant Log Output
$ stalwart-cli apply --file /etc/stalwart/initial-plan.ndjson --json
Plan: 0 destroy, 2 update, 10 create (10 objects)
{"op":"create","object":"Domain","index":0,"status":"error","error":"Domain: create failed for dom-mail (operation #1): error: primaryKeyViolation | Properties: name | Object id: Domain#b"}
{"op":"summary","plan":{"destroys":0,"updates":2,"creates":10,"create_objects":10},"done":{"destroyed":0,"updated":0,"created":0,"failed":1}}
error: Domain: create failed for dom-mail (operation #1): error: primaryKeyViolation | Properties: name | Object id: Domain#b
Server side: nothing logged (RUST_LOG=trace on a recovery-mode container shows no request received).
The actual culprit in crates/cli/src/commands/apply.rs around line 232:
match id {
Some(v) if !v.is_empty() => Ok(v.to_string()),
_ => Err(CliError::IdRequired(...)),
}
### Stalwart Version
v0.16.x
### Installation Method
Docker
### Database Backend
RocksDB
### Blob Storage
Filesystem
### Search Engine
Internal
### Directory Backend
LDAP
### Additional Context
Discovered while deploying a Terraform-managed Stalwart 0.16.4 instance on AWS (rocksdb DataStore on EBS-backed Ubuntu 24.04 t4g.medium ASG). Our initial-plan.ndjson was generated from a Terraform template that followed the docs literally; the apply step failed 5 retries in a row before we read the CLI source and discovered the docs-vs-code mismatch.
Suggested fixes (one or both):
Fix the docs at https://stalw.art/docs/management/cli/apply (including the Ansible / Terraform / NixOS / Pulumi integration examples on the same page — the Ansible example also uses the wrong shape).
Make the CLI error message specific: instead of "Domain requires an id", surface something like update op for 'Domain' is missing the top-level 'id' field (got: object='Domain', value=<inline dict>); see https://stalw.art/docs/management/cli/apply for the correct shape.
The full draft body (with source-line citations and additional context) is at https://gist.github.com/<not yet posted> if useful — happy to attach as a follow-up reply on this thread.
Note: this is a docs/CLI ergonomics bug, not a runtime SMTP issue — the form's example values assume a runtime/transport problem so several of the dropdowns above (Blob Storage / Search Engine) aren't strictly applicable but I picked reasonable defaults.
### I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.
on
### I have searched this support forum (open and closed topics) and confirm this is not a duplicate.
on
### I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I'd prefer a human-only reply, I'll add the no-ai tag to my topic.
on