Calendar shared via JMAP Calendar/set with Read Items / read-write is not propagated to the ACL index (SUBSPACE_ACL) — grantee never sees it (PostgreSQL backend)

Issue Description

A calendar shared via JMAP Calendar/set (from the web UI) at Read Items or read-write level is never visible to the grantee in any client. This is distinct from the known “Read Free/Busy is
a scheduling primitive, not a subscription” behaviour — it reproduces with grants that include Read/ReadItems, which per build_access_token should make the calendar visible. The data layer
(archive) and the inverted ACL index (SUBSPACE_ACL) are desynchronized: the archive records the grant, but the ACL index never receives it.

Expected Behavior

After sharing a calendar with mayReadItems (or higher), the grantee should get the owner as a secondary account in their JMAP session and see the calendar in their calendar list.
A row should exist in SUBSPACE_ACL: grant_account_id(grantee) ‖ owner_account_id(owner) ‖ collection=08 (Calendar) ‖ document_id, so that build_access_token → acl_query(AclQuery::HasAccess
{ grant_account_id }) returns the owner.

Actual Behavior

  • Calendar/get on the owner’s calendar correctly returns shareWith with the grantee’s grant (e.g. mayReadItems / mayWriteAll), and the JMAP state id changes on each share change → the
    archive (data layer) is updated.

  • SUBSPACE_ACL contains no row for grant=grantee / owner=owner / collection=08. The grant is never written to the ACL index.

  • Consequently the owner is never added as a secondary account in the grantee’s session, and the calendar never appears in any client (web UI, Bulwark, Thunderbird).

  • A separate, earlier calendar share on the same instance DOES have a correct SUBSPACE_ACL row (value 0x409 = Read|ReadItems|SchedulingReadFreeBusy) and that calendar IS visible — so the ACL
    index write path can work, it just doesn’t for these shares.

  • Toggling the permission (e.g. read → read/write) updates the archive (state id changes) but produces no change in SUBSPACE_ACL, suggesting archive and index stay permanently out of sync
    once diverged.

  • No SQL error in PostgreSQL logs and no WARN/ERROR in Stalwart logs (INFO level); Calendar/set commits cleanly.

Reproduction Steps

  1. As user A, share a calendar with user B from the web UI at “Read Items” or read-write level.
  2. Calendar/get (account A) → shareWith correctly lists B’s grant.
  3. Session (as B) → accounts does not include A.
  4. B does not see A’s calendar in any client.
  5. Inspect the store: SUBSPACE_ACL has no row for grant=B / owner=A / collection=08, even though A’s calendar archive contains the ACL.

Relevant Log Output

(INFO level: Calendar/set commits with no error; no store/JMAP/ACL warnings or errors.
PostgreSQL logs: no SQL error during the share.
Can re-run at trace level on request.)

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

PostgreSQL

Blob Storage

S3-compatible

Search Engine

PostgreSQL

Directory Backend

OIDC

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

I can’t reproduce it from a static read alone: the calendar update path does emit an ACL index value, and the diff that writes acl_grant / acl_revoke looks correct, so if SUBSPACE_ACL stays empty the failure is more specific than “the ACL write path is missing”, especially since you note an earlier share on the same instance has a correct row.

A trace-level log of the failing Calendar/set is exactly what I need: it’ll show whether the ACL diff branch runs and whether acl_grant is emitted. Also useful: the exact 0.16.x patch, the precise grant being set (which permission bits), and whether this calendar was created before an upgrade vs fresh on the current version, the working-earlier-share-vs-failing-new-ones pattern hints at a schema/migration difference.

Additional data from a trace-level capture of the failing operation.

Environment

Stalwart v0.16.7, official Docker image stalwartlabs/stalwart:v0.16.7, fresh install - never upgraded

Data store: PostgreSQL 18.4. Blob store: S3. Full-text search: Postgresql. Directory: external OIDC.

JMAP webmail client.

Setup

Owner alice (account id 2), grantee bob (account id 1).

Calendar shared: alice’s calendar (JMAP accountId:“c”, calendar object id “b”, name “Calendrier (aliceATexample.org)”).

Permission set: mayReadFreeBusy:true, mayReadItems:true, all others false.

Request sent (Calendar/set)

{“accountId”:“c”,“update”:{“b”:{“shareWith/b”:{“mayReadFreeBusy”:true,“mayReadItems”:true,“mayWriteAll”:false,“mayWriteOwn”:false,“mayUpdatePrivate”:false,“mayRSVP”:false,“mayShare”:false,“mayDelete”:false}}}}

Response (Calendar/set)

{“accountId”:“c”,“newState”:“smy”,“updated”:{“b”:null}}

Log: store.data-write elapsed = 114ms, total = 8.

Subsequent Calendar/get on the same calendar

{“id”:“b”,“name”:“Calendrier (aliceATexample.org)”,“shareWith”:{“b”:{“mayReadFreeBusy”:true,“mayReadItems”:true,“mayWriteAll”:true,“mayWriteOwn”:true,“mayUpdatePrivate”:true,“mayRSVP”:true,“mayShare”:false,“mayDelete”:false}}, …}

ACL index (PostgreSQL table a, full dump after the operation)

key = 00000005 00000001 08 00000002val = 0000000000000409

This is the only row in the table. Key layout: grant_account_id(u32) | owner_account_id(u32) | collection(1B) | document_id(u32); collection 0x08 = Calendar; value = permission bitmap.

No row exists with grant=00000001 (bob), owner=00000002 (alice), collection 0x08.

traces:


```
2026-06-16T19:09:05Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = [["host", "mail.example.org"], ["x-real-ip", "203.0.113.10"], ["x-forwarded-for", "203.0.113.10"], ["x-forwarded-proto", "https"], ["content-length", "376"], ["user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0"], ["accept", "*/*"], ["accept-language", "en-US,en;q=0.9"], ["accept-encoding", "gzip, deflate, br, zstd"], ["content-type", "application/json"], ["referer", "hxxps://webmail.example.org/"], ["origin", "hxxps://webmail.example.org"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["authorization", "Bearer ***REDACTED***"], ["dnt", "1"], ["sec-gpc", "1"], ["priority", "u=4"]], contents = "{"using":["urn_ietf_params:jmap:core","urn_ietf_params:jmap:calendars","urn_ietf_params:jmap:principals:owner"],"methodCalls":[["Calendar/get",{"accountId":"c","properties":["id","name","description","color","sortOrder","isSubscribed","isVisible","isDefault","includeInAvailability","defaultAlertsWithTime","defaultAlertsWithoutTime","timeZone","shareWith","myRights"]},"0"]]}", size = 376
2026-06-16T19:09:05Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) accountId = 2, collection = "calendar", changeId = 96, elapsed = 0ms
2026-06-16T19:09:05Z DEBUG JMAP method call (jmap.method-call) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Calendar/get", accountId = 2, elapsed = 0ms
2026-06-16T19:09:05Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = "{"methodResponses":[["Calendar/get",{"accountId":"c","state":"sma","list":[{"id":"b","name":"Calendrier ([email protected])","description":null,"color":"#3b82f6","sortOrder":0,"isSubscribed":false,"isVisible":true,"isDefault":true,"includeInAvailability":"all","defaultAlertsWithTime":{},"defaultAlertsWithoutTime":{},"timeZone":null,"shareWith":{"b":{"mayReadFreeBusy":true,"mayReadItems":true,"mayWriteAll":true,"mayWriteOwn":true,"mayUpdatePrivate":true,"mayRSVP":true,"mayShare":false,"mayDelete":false}},"myRights":{"mayReadFreeBusy":true,"mayReadItems":true,"mayWriteAll":true,"mayWriteOwn":true,"mayUpdatePrivate":true,"mayRSVP":true,"mayShare":true,"mayDelete":true}}],"notFound":[]},"0"]],"sessionState":"48cce36b"}", code = 200, size = 727
2026-06-16T19:09:05Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 58862, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:05Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 58862, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:05Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 58862, contents = (null), code = 204, size = 0
2026-06-16T19:09:05Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:05Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "accessToken"
2026-06-16T19:09:05Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = [["host", "mail.example.org"], ["x-real-ip", "203.0.113.10"], ["x-forwarded-for", "203.0.113.10"], ["x-forwarded-proto", "https"], ["content-length", "259"], ["user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0"], ["accept", "*/*"], ["accept-language", "en-US,en;q=0.9"], ["accept-encoding", "gzip, deflate, br, zstd"], ["content-type", "application/json"], ["referer", "hxxps://webmail.example.org/"], ["origin", "hxxps://webmail.example.org"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["authorization", "Bearer ***REDACTED***"], ["dnt", "1"], ["sec-gpc", "1"], ["priority", "u=4"]], contents = "{"using":["urn_ietf_params:jmap:core","urn_ietf_params:jmap:calendars","urn_ietf_params:jmap:principals:owner"],"methodCalls":[["CalendarEvent/query",{"accountId":"c","filter":{"after":"2026-06-16T00:00:00","before":"2026-07-16T23:59:59"},"limit":1000},"0"]]}", size = 259
2026-06-16T19:09:05Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) accountId = 2, collection = "calendar", changeId = 96, elapsed = 0ms
2026-06-16T19:09:05Z DEBUG JMAP method call (jmap.method-call) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "CalendarEvent/query", accountId = 2, elapsed = 0ms
2026-06-16T19:09:05Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = "{"methodResponses":[["CalendarEvent/query",{"accountId":"c","queryState":"saq","canCalculateChanges":true,"position":0,"ids":[]},"0"]],"sessionState":"48cce36b"}", code = 200, size = 161
2026-06-16T19:09:05Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:05Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "accessToken"
2026-06-16T19:09:05Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = [["host", "mail.example.org"], ["x-real-ip", "203.0.113.10"], ["x-forwarded-for", "203.0.113.10"], ["x-forwarded-proto", "https"], ["content-length", "719"], ["user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0"], ["accept", "*/*"], ["accept-language", "en-US,en;q=0.9"], ["accept-encoding", "gzip, deflate, br, zstd"], ["content-type", "application/json"], ["referer", "hxxps://webmail.example.org/"], ["origin", "hxxps://webmail.example.org"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["authorization", "Bearer ***REDACTED***"], ["dnt", "1"], ["sec-gpc", "1"], ["priority", "u=4"]], contents = "{"using":["urn_ietf_params:jmap:core","urn_ietf_params:jmap:calendars","urn_ietf_params:jmap:principals:owner"],"methodCalls":[["CalendarEvent/query",{"accountId":"c","filter":{"types":["Task"]},"limit":1000},"0"],["CalendarEvent/get",{"accountId":"c","properties":["id","@type","uid","calendarIds","title","description","descriptionContentType","created","updated","start","due","duration","timeZone","showWithoutTime","utcStart","utcEnd","progress","progressUpdated","priority","privacy","color","keywords","categories","recurrenceRule","recurrenceOverrides","excludedRecurrenceRule","useDefaultAlerts","alerts","relatedTo","percentComplete"],"#ids":{"resultOf":"0","name":"CalendarEvent/query","path":"/ids"}},"1"]]}", size = 719
2026-06-16T19:09:05Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) accountId = 2, collection = "calendar", changeId = 96, elapsed = 0ms
2026-06-16T19:09:05Z DEBUG Unsupported JMAP filter (jmap.unsupported-filter) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = "types", accountId = 2, causedBy = "CalendarEvent/query"
2026-06-16T19:09:05Z DEBUG Invalid JMAP result reference (jmap.invalid-result-reference) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = "Result reference to 0#CalendarEvent/query not found."
2026-06-16T19:09:05Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = "{"methodResponses":[["error",{"type":"unsupportedFilter","description":"REDACTED"},"0"],["error",{"type":"invalidResultReference","description":"REDACTED"},"1"]],"sessionState":"48cce36b"}", code = 200, size = 229
2026-06-16T19:09:05Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:05Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "accessToken"
2026-06-16T19:09:05Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = [["host", "mail.example.org"], ["x-real-ip", "203.0.113.10"], ["x-forwarded-for", "203.0.113.10"], ["x-forwarded-proto", "https"], ["content-length", "376"], ["user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0"], ["accept", "*/*"], ["accept-language", "en-US,en;q=0.9"], ["accept-encoding", "gzip, deflate, br, zstd"], ["content-type", "application/json"], ["referer", "hxxps://webmail.example.org/"], ["origin", "hxxps://webmail.example.org"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["authorization", "Bearer ***REDACTED***"], ["dnt", "1"], ["sec-gpc", "1"], ["priority", "u=4"]], contents = "{"using":["urn_ietf_params:jmap:core","urn_ietf_params:jmap:calendars","urn_ietf_params:jmap:principals:owner"],"methodCalls":[["Calendar/get",{"accountId":"e","properties":["id","name","description","color","sortOrder","isSubscribed","isVisible","isDefault","includeInAvailability","defaultAlertsWithTime","defaultAlertsWithoutTime","timeZone","shareWith","myRights"]},"0"]]}", size = 376
2026-06-16T19:09:05Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) accountId = 4, collection = "calendar", changeId = 5, elapsed = 0ms
2026-06-16T19:09:05Z DEBUG JMAP method call (jmap.method-call) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Calendar/get", accountId = 2, elapsed = 1ms
2026-06-16T19:09:05Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = "{"methodResponses":[["Calendar/get",{"accountId":"e","state":"sau","list":[{"id":"b","name":"Calendrier ([email protected])","description":null,"color":null,"sortOrder":0,"isSubscribed":false,"isVisible":true,"isDefault":true,"includeInAvailability":"all","defaultAlertsWithTime":{},"defaultAlertsWithoutTime":{},"timeZone":null,"shareWith":{},"myRights":{"mayReadFreeBusy":true,"mayReadItems":true,"mayWriteAll":true,"mayWriteOwn":true,"mayUpdatePrivate":true,"mayRSVP":true,"mayShare":true,"mayDelete":true}}],"notFound":[]},"0"]],"sessionState":"48cce36b"}", code = 200, size = 562
2026-06-16T19:09:05Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:05Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "accessToken"
2026-06-16T19:09:05Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = [["host", "mail.example.org"], ["x-real-ip", "203.0.113.10"], ["x-forwarded-for", "203.0.113.10"], ["x-forwarded-proto", "https"], ["content-length", "259"], ["user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0"], ["accept", "*/*"], ["accept-language", "en-US,en;q=0.9"], ["accept-encoding", "gzip, deflate, br, zstd"], ["content-type", "application/json"], ["referer", "hxxps://webmail.example.org/"], ["origin", "hxxps://webmail.example.org"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["authorization", "Bearer ***REDACTED***"], ["dnt", "1"], ["sec-gpc", "1"], ["priority", "u=4"]], contents = "{"using":["urn_ietf_params:jmap:core","urn_ietf_params:jmap:calendars","urn_ietf_params:jmap:principals:owner"],"methodCalls":[["CalendarEvent/query",{"accountId":"e","filter":{"after":"2026-06-16T00:00:00","before":"2026-07-16T23:59:59"},"limit":1000},"0"]]}", size = 259
2026-06-16T19:09:05Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) accountId = 4, collection = "calendar", changeId = 5, elapsed = 0ms
2026-06-16T19:09:05Z DEBUG JMAP method call (jmap.method-call) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "CalendarEvent/query", accountId = 2, elapsed = 0ms
2026-06-16T19:09:05Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = "{"methodResponses":[["CalendarEvent/query",{"accountId":"e","queryState":"sau","canCalculateChanges":true,"position":0,"ids":[]},"0"]],"sessionState":"48cce36b"}", code = 200, size = 161
2026-06-16T19:09:05Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:05Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "accessToken"
2026-06-16T19:09:05Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = [["host", "mail.example.org"], ["x-real-ip", "203.0.113.10"], ["x-forwarded-for", "203.0.113.10"], ["x-forwarded-proto", "https"], ["content-length", "576"], ["user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0"], ["accept", "*/*"], ["accept-language", "en-US,en;q=0.9"], ["accept-encoding", "gzip, deflate, br, zstd"], ["content-type", "application/json"], ["referer", "hxxps://webmail.example.org/"], ["origin", "hxxps://webmail.example.org"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["authorization", "Bearer ***REDACTED***"], ["dnt", "1"], ["sec-gpc", "1"], ["priority", "u=4"]], contents = "{"using":["urn_ietf_params:jmap:core","urn_ietf_params:jmap:calendars","urn_ietf_params:jmap:principals:owner"],"methodCalls":[["CalendarEvent/get",{"accountId":"c","ids":null,"properties":["id","@type","uid","calendarIds","title","description","descriptionContentType","created","updated","start","due","duration","timeZone","showWithoutTime","utcStart","utcEnd","progress","progressUpdated","priority","privacy","color","keywords","categories","recurrenceRule","recurrenceOverrides","excludedRecurrenceRule","useDefaultAlerts","alerts","relatedTo","percentComplete"]},"0"]]}", size = 576
2026-06-16T19:09:05Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) accountId = 2, collection = "calendar", changeId = 96, elapsed = 0ms
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "account"
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "domainId"
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 4, collection = "account"
2026-06-16T19:09:05Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "domainId"
2026-06-16T19:09:05Z DEBUG JMAP method call (jmap.method-call) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "CalendarEvent/get", accountId = 2, elapsed = 0ms
2026-06-16T19:09:05Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = "{"methodResponses":[["CalendarEvent/get",{"accountId":"c","state":"saq","list":[],"notFound":[]},"0"]],"sessionState":"48cce36b"}", code = 200, size = 129
2026-06-16T19:09:09Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:09Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "accessToken"
2026-06-16T19:09:09Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:09Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "account"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "account"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 3, collection = "account"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 4, collection = "account"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 5, collection = "account"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 6, collection = "account"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 7, collection = "account"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 8, collection = "account"
2026-06-16T19:09:09Z DEBUG Cache hit (store.cache-hit) key = 9, collection = "account"
2026-06-16T19:09:11Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:11Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:11Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = (null), code = 204, size = 0
2026-06-16T19:09:11Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:11Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:11Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "accessToken"
2026-06-16T19:09:11Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = [["host", "mail.example.org"], ["x-real-ip", "203.0.113.10"], ["x-forwarded-for", "203.0.113.10"], ["x-forwarded-proto", "https"], ["content-length", "203"], ["user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0"], ["accept", "*/*"], ["accept-language", "en-US,en;q=0.9"], ["accept-encoding", "gzip, deflate, br, zstd"], ["content-type", "application/json"], ["referer", "hxxps://webmail.example.org/"], ["origin", "hxxps://webmail.example.org"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["authorization", "Bearer ***REDACTED***"], ["dnt", "1"], ["sec-gpc", "1"], ["priority", "u=0"]], contents = "{"using":["urn_ietf_params:jmap:core","urn_ietf_params:jmap:calendars","urn_ietf_params:jmap:principals:owner"],"methodCalls":[["Calendar/set",{"accountId":"c","update":{"b":{"shareWith/b":null}}},"0"]]}", size = 203
2026-06-16T19:09:11Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:11Z DEBUG Cache hit (store.cache-hit) accountId = 2, collection = "calendar", changeId = 96, elapsed = 0ms
2026-06-16T19:09:11Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:11Z TRACE Write batch operation (store.data-write) elapsed = 34ms, total = 8
2026-06-16T19:09:11Z DEBUG JMAP method call (jmap.method-call) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Calendar/set", accountId = 2, elapsed = 35ms
2026-06-16T19:09:11Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = "{"methodResponses":[["Calendar/set",{"accountId":"c","newState":"smu","updated":{"b":null}},"0"]],"sessionState":"48cce36b"}", code = 200, size = 124
2026-06-16T19:09:11Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:16Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, remoteIp = 203.0.113.10, url = "/jmap/"
2026-06-16T19:09:16Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-06-16T19:09:16Z DEBUG Cache hit (store.cache-hit) key = 2, collection = "accessToken"
2026-06-16T19:09:16Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, details = [["host", "mail.example.org"], ["x-real-ip", "203.0.113.10"], ["x-forwarded-for", "203.0.113.10"], ["x-forwarded-proto", "https"], ["content-length", "359"], ["user-agent", "Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0"], ["accept", "*/*"], ["accept-language", "en-US,en;q=0.9"], ["accept-encoding", "gzip, deflate, br, zstd"], ["content-type", "application/json"], ["referer", "hxxps://webmail.example.org/"], ["origin", "hxxps://webmail.example.org"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["authorization", "Bearer ***REDACTED***"], ["dnt", "1"], ["sec-gpc", "1"], ["priority", "u=0"]], contents = "{"using":["urn_ietf_params:jmap:core","urn_ietf_params:jmap:calendars","urn_ietf_params:jmap:principals:owner"],"methodCalls":[["Calendar/set",{"accountId":"c","update":{"b":{"shareWith/b":{"mayReadFreeBusy":true,"mayReadItems":true,"mayWriteAll":false,"mayWriteOwn":false,"mayUpdatePrivate":false,"mayRSVP":false,"mayShare":false,"mayDelete":false}}}},"0"]]}", size = 359
2026-06-16T19:09:16Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:16Z DEBUG Cache update (store.cache-update) accountId = 2, collection = "calendar", changeId = 101, details = 1, total = 1, elapsed = 1ms
2026-06-16T19:09:16Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-06-16T19:09:16Z TRACE Write batch operation (store.data-write) elapsed = 114ms, total = 8
2026-06-16T19:09:16Z DEBUG JMAP method call (jmap.method-call) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, id = "Calendar/set", accountId = 2, elapsed = 116ms
2026-06-16T19:09:16Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.0.0.2, remotePort = 50704, contents = "{"methodResponses":[["Calendar/set",{"accountId":"c","newState":"smy","updated":{"b":null}},"0"]],"sessionState":"48cce36b"}", code = 200, size = 124

```

A lot of the analysis here looks like it came from an AI model reading the source, and it is incorrectly analyzing things. The references to SUBSPACE_ACL, build_access_token, AclQuery::HasAccess and the “archive and index are permanently desynchronized” conclusion are inferences, not something the symptom actually proves, and they are sending the investigation down a very specific rabbit hole that the evidence does not support.

What is genuinely useful in your report is the observable part: as user B, the session does not list A, and B cannot see A’s calendar, even though Calendar/get on A shows B’s grant. That is a real thing to chase. The rest is a guess about the internal cause.

On that guess: the Calendar/set sharing path writes the archive and the ACL index together, in the same atomic batch, and this is covered by the integration tests for calendars, address books and files. A fresh share of A to B does grant access and does populate the index in those tests. So there is no general defect in the write path that would explain a brand new share landing in the archive but not the index. The single row you dumped is also an unrelated share, not the A to B grant you were testing, so it does not actually show the missing row, only that the table has one entry.

A much more likely explanation for “B’s session does not include A” is the access token cache for B not being refreshed, rather than anything in the ACL table. If B was holding a session or token from before the share, it would not pick up the new grant until invalidated.

To get anywhere, could you try a clean reproduction:

  1. Create two brand new accounts, A and B.
  2. As A, share a calendar with B once.
  3. As B, open a fresh session and check whether A appears.

If that fails on fresh accounts, please share the exact requests and responses for those three steps. Also let me know your deployment: single node or cluster, and whether this instance was ever restored, imported or migrated. If it only happens on your existing data and not on fresh accounts, it is most likely pre-existing state on that instance rather than a bug in the current sharing code.

it’s a fresh install, no migration/restoration for a single server deployment with postgres only (both data and search). You are right the llm did review the source code, but it also did dig the postgres database in parallel and despite both ACL and archive are updated atomically in the code, likely the ACL entry was not updated while only the archive was. Might be the llm did not dig the ACL the right way.

I can confirm the same behavior still happens during the 3 steps on brand new accounts and calendars on the same instance.

Same as well @stalwart

Open a new topic including reproduction steps. This post was marked as solved as it contains AI hallucinations.