Conversation
c8d32a7 to
d8bd743
Compare
|
|
||
| - Organization contacts and groups are stored on the organization instance only, when the organization has one. | ||
| - When the context has a contacts side service, organization contacts come from `twake:contacts:common`, and the stack's `user.created` and `domain.user.deleted` handlers stop writing contacts. | ||
| - Personal contacts stay on the user's instance. |
There was a problem hiding this comment.
I don't know if the sharing process & sharing contacts has been written in the common contacts ADR. Can you check that?
There was a problem hiding this comment.
no it hasn't been mentioned in the common contact ADR ( not in details ), it was just mentioned that the shared drive will be a use case to consume twake:contacts:common exchange. i added the exact details in this ADR
|
|
||
| - The stack adds a consumer on `twake:contacts:common`. A message with `audience.domain` writes the contact on the org instance of that domain, one with `audience.user` on that user's instance. | ||
| - `user.created` (queue `stack.user.created`) and `domain.user.deleted` (queue `stack.b2b.user.deleted`) no longer write contacts. `user.created` is still consumed for the passphrase and the Matrix ID. | ||
| - `b2b.group.*` (queue `stack.b2b.group.lifecycle`) is applied to the org instance only, until groups arrive on `twake:contacts:common`. A member added to a group before their contact arrives is created from the group event, then updated. |
There was a problem hiding this comment.
how can we assure that we've the same contacts at the end?
There was a problem hiding this comment.
- The stack consumer will be ( and should be ) idempotent: ADD/UPDATE will carry the full contact, and documents are keyed by CardDAV path, so replaying a message changes nothing.
- The contact side service can republish the entire address book ( ADR 43 ), which the stack uses to either fill a new org instance or repair contacts the stack missed (RabbitMQ failures maybe )
There was a problem hiding this comment.
Maybe we should have a reconciliation job just in case something was lost in mq or anywhere
6a17934 to
b9829dc
Compare
b9829dc to
c5e5a24
Compare
| - `audience.user`: a personal contact, written on that user's instance. | ||
| - Empty audience: dropped. | ||
|
|
||
| Members reach the domain address book through the side service, which consumes `user.created` ([ADR 058](https://github.com/linagora/twake-workplace-private/pull/1640)). The member's cozy URL comes from `x-twake-workplace-fqdn` in the JSContact `vCardProps`. |
There was a problem hiding this comment.
how it's connected to the stack and this ADR? do we need to take care of vCard?
There was a problem hiding this comment.
since you receive a Vcard as a contact, the stack needs to parse the x-twake-workplace-fqdn to resolve the contact workspace url ( instance URL )
There was a problem hiding this comment.
but where I recieve Vcard as a contacts and why? in rabbitmq ?
There was a problem hiding this comment.
yes via RabbitMQ, it's mentioned in the "Where members and contacts come from"
|
|
||
| A personal contact only names its owner by email, and the stack cannot find an instance from an email today. So the `user.created` handler stores `internalEmail` on the instance. It writes no contact anymore, and still sets the passphrase, the vault keys and the Matrix ID. | ||
|
|
||
| A document is keyed by its CardDAV `path`, because the same `uid` can sit in several address books. |
There was a problem hiding this comment.
the same for this, what document, what CardDAV?
There was a problem hiding this comment.
Here, it means the uniqueness is guaranteed by the path of the contact in cardav
There was a problem hiding this comment.
ok, so CardDAV is the main protocal of communication between services and "common contacts", and we rely on CardDAV path as and identifier?
There was a problem hiding this comment.
that's the unique identifier we have in sabre yes
| When it matches nothing, the stack shares with the email, which sends an invitation, and collects the address: | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram |
There was a problem hiding this comment.
we are still mixing things in this ADR, to have contacts only on org instance and contacting "contact side service"
There was a problem hiding this comment.
The collected flow is here because it decides who writes the contact document ( and after this ADR it is no longer the stack )
There was a problem hiding this comment.
mmm, why? it shouldn't be stack, it a stack's database, nobody except stack should write there if we are talking about the same thing
There was a problem hiding this comment.
the stack will write there but only if it confirms that contact is replicated and available globally. so it's writing with extra verification steps
There was a problem hiding this comment.
I don't think that we need this remote transaction here, and for example, rollback sharing if the contact service is not available, or wait for the rabbitmq update, we can do it only if we consider io.cozy.contacts only as cache. I'll check how do we use it during sharing, but at least we are storing "trusted" flag there, and it will be strange to wait for it with remote rabbitmq call
There was a problem hiding this comment.
Hmm, you are right. i updated the ADR
| - A member change is one write instead of one per instance. | ||
| - Sharing between members depends on the org instance. | ||
|
|
||
| ## Open questions |
There was a problem hiding this comment.
Whan more open question that should be addressed in this ADR, what is external identifier for contact and group? the same uuid that we have right now?
There was a problem hiding this comment.
Good question; I do not want to / can't decide it alone. For contacts, we have the CardDAV path; for groups, nothing has been decided yet, and @chibenwa prefers that it should be tackled another time ( it can be labels on a contact for example )
| - When a member leaves a group, what happens to the sharings using it? Today the `share-group` trigger removes them. | ||
| - Existing group sharings use `b2b-group-<hash>` ids. How do they keep matching their groups? | ||
| - Domain groups can have thousands of members, and the stack reads at most 1000 per group, do we need to change that? | ||
| - A message delivered out of order can overwrite a newer contact until the next republication. we need to have some sort of revision id or time stamping from sabre? |
There was a problem hiding this comment.
yes, if we want consistency
eventually)
c5e5a24 to
ce57e1e
Compare
efeb673 to
f50890d
Compare
f50890d to
6153f4c
Compare
Status
Proposed
Date
2026-09-14
Context
io.cozy.contacts, and today copies every member and group into every instance of the organization.The common contacts ADR makes Sabre the source of truth: apps read contacts from
twake:contacts:commonexchange and publish the ones they collect ontwake:contacts:collectedexchange.Sharing reads contacts on the caller's instance to add recipients, to follow group changes and to auto accept. That breaks once members are no longer copied there.
Decision
twake:contacts:commonexchange. The stack still writes one itself when it needs it immediately (sharing with an unknown email, answering a sharing), and the feed updates it when it comes back.Instances without an
OrgIDare unchanged.Where members and contacts come from
flowchart LR subgraph rabbitmq[RabbitMQ] sabreEx{{exchanges sabre:contact:*}} common{{exchange twake:contacts:common}} end sabre[(Sabre)] --> sabreEx sabreEx --> css[contacts side service] css -->|JSContact + audience| common common --> stack[cozy-stack] common --> others[chat, mail, ...] stack -->|audience.domain| org[(org instance)] stack -->|audience.user| usr[(user instance)]twake:contacts:commonis afanoutexchange with no routing key, so the stack receives every change for every user and domain. The message says what to do:action:ADD,UPDATEorDELETE.ADDandUPDATEcarry the full contact, so replaying one changes nothing.audience.domain: a member, written on the org instance.audience.user: a personal contact, written on that user's instance.Members reach the domain address book through the side service, which consumes
user.created(ADR 058).The contact comes as JSContact. The vCard properties JSContact does not cover stay in
vCardProps, and the stack readsx-twake-workplace-fqdnthere to fill the contact'scozyURL, the address sharing uses to reach that member's instance.A personal contact only names its owner by email, and the stack cannot find an instance from an email today. So the
user.createdhandler storesinternalEmailon the instance. It writes no contact anymore, and still sets the passphrase, the vault keys and the Matrix ID.Each message carries the CardDAV
pathof the contact: its address book and its file name. The stack stores that path and uses it to find which document the message is about. Theuidis not enough, the same one can sit in several address books.When a contact changes
cozyURL,trustedForSharing). Existing sharings keep the name and email they were created with.Sharing with an email
This section is about who writes the contact document. Finding people to share with is the suggestions ADR.
When the email matches a contact, on the org instance first, the member gets its name and instance URL.
When it matches nothing, the stack creates the contact on the user's instance, shares with it, which sends an invitation, and collects the address:
The contact created at share time has no CardDAV path yet, it only holds the email. So for that first message the stack matches on the email and fills in the path, the name and the rest of the card. From then on the document is keyed by its path like any other.
Sharing never waits for any of this. The document exists before the invitation goes out, the publish is fire and forget, and a contacts service that is down changes nothing for the sharer: no rollback, no retry loop. The contact stays as the user typed it until Sabre answers.
The detour matters because Sabre is the source of truth: the address ends up in the user's collected address book, so the same contact reaches the other apps and not only the stack.
In standalone mode there is no feed, so the contact stays as the stack created it ( same as today ).
The document is not only a copy of Sabre.
trustedForSharingand thecozyURL belong to the stack, and a message from the feed never overwrites them.twake:contacts:collectedis also afanoutexchange. The stack publisher refuses an empty routing key, so it needs a change. ( some refactor needed )Auto accept
A sharer is trusted when their instance has the same
OrgIDas the recipient.trustedForSharingstays for people outside the organization.Answering a sharing is the other place where the stack writes a contact itself: it creates the sender's contact on the recipient instance and marks it trusted. That stays for people outside the organization. Between members, the
OrgIDdecides and nothing is written.Staying in sync
The side service can republish every address book: every contact in Sabre is sent again as an
ADD. The stack writes them like any other message, which fills a new org instance or fixes contacts it missed. Then it deletes the old copies on member instances withBulkDeleteDocs, so theshare-grouptrigger does not revoke group members.A republication repairs a lost
ADDorUPDATE, not a lostDELETE. To catch those, the stack wants a periodic reconciliation on the org instance: compare what Sabre sends with what the instance holds, and remove the rest. That needs the republication to be a full set the stack can trust, which is the open question below.Consequences
Open questions
twake:contacts:common(maybe as a label on the contact)? Until then, the stack keeps building them fromb2b.group.*.share-grouptrigger removes them.b2b-group-<hash>ids. How do they keep matching their groups?DELETEis not repaired: the republication only sendsADD, so a contact deleted in Sabre stays in the stack. Does the side service need a full sync mode, or does the stack remove contacts that a republication did not send?