Skip to content

docs: add contact suggestions adr - #4915

Open
rezk2ll wants to merge 1 commit into
masterfrom
chore/contact-suggestions-adr
Open

rezk2ll wants to merge 1 commit into
masterfrom
chore/contact-suggestions-adr

Conversation

@rezk2ll

@rezk2ll rezk2ll commented Sep 14, 2026

Copy link
Copy Markdown
Member

Status

Proposed

Date

2026-09-14

Context

Recipient suggestions in Drive have three problems:

  1. They do not scale. The share modal loads every contact and group of the instance each time it opens, page by page, then filters them in the browser. It only works because every member is copied to every instance, which is what the storage ADR stops doing. Cached or not, an instance holding thousands of member documents so the browser can filter them is the problem.
  2. They differ from the other apps. Mail and Calendar search their own index, Drive filters its local copy, so a colleague found in Mail can be missing in Drive.
  3. There is no contact search route and no index on contact names, so the stack cannot answer instead of the client.

The common contacts ADR already syncs contacts to every app through the twake:contacts:common exchange. ADR 058 adds a new contacts service with a search API shared by every app. This ADR describes how the stack uses that API. Where members and contacts are stored is covered by the storage ADR.

Decision

  • The stack adds a GET /contacts/suggest route, called by the share modal as the user types.
  • The route calls the search API of the contacts service, asking only for people with an email, and maps each result to a contact document by its CardDAV path.
  • In standalone mode (no contacts service, so no common contacts either), the route searches the org instance and the user's instance directly.
sequenceDiagram
  participant M as share modal
  participant S as cozy-stack
  participant C as contacts service
  participant I as org or user instance

  M->>S: GET /contacts/suggest?q=dup
  S->>C: search "dup", with an email, for the user
  C-->>S: vCards with their CardDAV path
  S->>I: contacts by path
  I-->>S: contacts
  S-->>M: suggestions
Loading

The search API

The search is proposed in twake-calendar-side-service#1061: POST /contacts/api/contacts/search?limit=&offset= with a query and a list of address books. It searches each address book in Sabre, merges the results and returns one vCard per contact, with its CardDAV path in _links.self.href.

What the stack needs from it:

  • A filter next to the text. Drive asks for "matches dup and has an email", and the service applies it before cutting to the top N. If the stack filters instead, the list comes up short: 30 results with 20 of them without an email leave 10 suggestions, and the next matches are never fetched.
  • A way to search every address book the user can see (their own, the collected one, the domain one) without listing them. Bootstrap the /metrics endpoint #1061 takes Sabre address book ids and user ids in the body, and the stack knows neither.
  • It knows which user is searching. Results depend on who asks, and the bearer identifies the stack, not the user, so the stack names the user in a header.
  • It takes the bearer of the stack's own OIDC client, one client per app (ADR 058).
  • Each result carries its CardDAV path, as Bootstrap the /metrics endpoint #1061 does. From the vCard the stack only reads the name and the emails.
  • Results come in the order to show them.
  • It only returns people the user may see. The stack does not filter them again.

Each context configures the full URL of the search endpoint and the OIDC client of the stack:

contexts:
  my-context:
    contacts_service:
      search_url: https://contacts-side-service.example.com/<search endpoint>
      client_id: cozy-stack
      client_secret: s3cret3
      token_url: https://identity-provider/path/to/token

A context without contacts_service is in standalone mode.

The stack stores the CardDAV path of every contact it receives (storage ADR). It looks each result up by that path, on the org instance first, then on the user's instance. Where it is found gives its kind: a member on the org instance, a personal contact on the user's instance.

A contact the stack created at share time has no path until Sabre sends it back. When the path matches nothing, the stack tries the email with the contacts-by-email view.

A contact can have no email, and someone without one cannot be invited. With the filter the contacts service never returns them, and the standalone search applies the same rule. The share modal already hides them today, it only lists contacts with an email or a cozy URL.

The route

GET /contacts/suggest?q=<text>&limit=<n> on the user's instance, with GET on io.cozy.contacts. q needs at least 3 characters and limit defaults to 20.

{
  "data": [
    {
      "type": "io.cozy.contacts",
      "id": "6f2a",
      "attributes": {
        "fullname": "Jean Dupont",
        "email": [{ "address": "jean.dupont@org.tld", "primary": true }],
        "cozy": [{ "url": "https://jdupont.org.tld", "primary": true }]
      },
      "meta": { "kind": "member" }
    }
  ]
}
  • Results keep the contacts service order.
  • Attributes use the io.cozy.contacts field names, so cozy-sharing can render them as today.
  • meta.kind is member or contact, depending on where the document was found.
  • A result with no matching document comes back without an id or kind, with the name and email from the contacts service. The client shares it by email.

Standalone mode

Without a contacts service, the stack runs a case-insensitive $regex Mango query on names and emails, on the org instance and the user's instance.

No existing index helps: contacts-by-email is an exact lookup, by-groups and by-me answer other reads, and a $regex cannot use an index anyway. So it stays a scan, which is fine on a standalone instance with few contacts.

What changes for the clients

  • cozy-sharing stops loading every contact and group, and calls GET /contacts/suggest as the user types (debounced, 3 characters minimum).
  • cozy-sharing sends people as {email}. Today, when the typed email matches nothing, it creates the contact itself so the recipient has a document to point at. It stops, and the stack does it instead: it creates the contact, shares, publishes the address on twake:contacts:collected, then updates that document when Sabre sends it back (storage ADR). Nothing waits on the contacts service.

What to do when things fail

  • Contacts service down or slow: after a short timeout, the route falls back to the standalone search and logs it.
  • Org instance unreachable: members come back without an id and are shared by email.

Consequences

  • Opening the share modal no longer downloads the organization.
  • Drive suggests the same people as the other apps.
  • Suggestions depend on the contacts service being up.

Open questions

  • How does the stack ask for "has an email" in the Bootstrap the /metrics endpoint #1061 request? @chibenwa
  • Can the address books be left out of the request, to mean every address book the user can see? @chibenwa
  • Which header names the user, and with which value (internalEmail)? @chibenwa
  • Will groups be searchable, and how? #1016 adds contact lists with their members to people search. Do we display the whole group (one entry) or flatten the members list?
  • What is the maximum limit, and what latency should the stack expect per keystroke?
  • The modal shows a member count for groups, but neither a result nor the group document carries it. Do we drop it, or store a count on the group?

@rezk2ll
rezk2ll force-pushed the chore/contact-suggestions-adr branch 2 times, most recently from 46e5b10 to 9f82cef Compare September 14, 2026 10:05
@rezk2ll
rezk2ll force-pushed the chore/contact-suggestions-adr branch from 013d0bc to 23db50c Compare September 14, 2026 14:35
@rezk2ll
rezk2ll requested a review from chibenwa September 15, 2026 10:46
@rezk2ll

rezk2ll commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

cc @chibenwa what do you think of the requirements of the API?

cc @Crash-- @shepilov

@rezk2ll
rezk2ll force-pushed the chore/contact-suggestions-adr branch 2 times, most recently from 5b58ac1 to f8f522b Compare September 15, 2026 13:15
Comment thread docs/adr/005-adr-contact-suggestions.md Outdated

Recipient suggestions in Drive have three problems:

1. They do not scale. The share modal [loads every contact and group](https://github.com/cozy/cozy-libs/blob/319c4890af0fbee641e25a2cb7c1bd2501b57024/packages/cozy-sharing/src/components/ShareRecipientsInput.jsx#L37) of the instance each time it opens, then [filters them in the browser](https://github.com/cozy/cozy-libs/blob/319c4890af0fbee641e25a2cb7c1bd2501b57024/packages/cozy-sharing/src/components/ShareAutosuggest.jsx#L43). In an organization of thousands of members, that is thousands of documents downloaded before the user types anything.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it lods them from local DB, if we want to support offline mode(I'm not sure that it's relevant for sharing) we still needs to keep this cache

Comment thread docs/adr/005-adr-contact-suggestions.md Outdated
Comment thread docs/adr/005-adr-contact-suggestions.md Outdated
Comment thread docs/adr/005-adr-contact-suggestions.md Outdated

- It knows which user is searching. Results depend on who asks (their personal contacts, the members of their organization), and the service token identifies the stack, not the user, so the stack names the user in a header.
- It accepts a service token, configured per context.
- Each result carries a display name and the emails.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phone? other contact fields?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think we will need that, we will invite/search by email or display name, and inviting by phone can arrive later

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when we will do this match contact->user and vice versa, email is just one of the identifiers

Comment thread docs/adr/005-adr-contact-suggestions.md
Comment thread docs/adr/005-adr-contact-suggestions.md Outdated
## What changes for the clients

- cozy-sharing stops loading every contact and group, and calls `GET /contacts/suggest` as the user types (debounced, 3 characters minimum).
- cozy-sharing sends people as `{email}` and stops creating contacts before sharing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why by email if we don't have an email? Do we create contact now from UI before sharing? It's not a stack who creating contact?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stack shares with the email sends the contact collected event, and the contact comes back from Sabre through the contacts service. and we do this because sabre is our source of truth and we want to make sure this contact is available to everyone else in every app and not just stack

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm, it depends, I don't remeber how contact is used during sharing(probably we need instance URL for autoacceptance flow, key exchange, and etc), and we already storing or can store in the member, but if we have to have a contacts for this, it's ok to store contact reacotd at first, and then update in with info from conact service.
Especially that we wil have standalone mode, and to have external http call in the middle of "transaction" ...
So I think then during the sharing we should create contact all (but we use it for autoacceptance) or call contact sercive only after sharing was created

@rezk2ll
rezk2ll force-pushed the chore/contact-suggestions-adr branch from f8f522b to aa9e13c Compare September 16, 2026 09:55
@rezk2ll
rezk2ll requested a review from shepilov September 16, 2026 11:11
@rezk2ll
rezk2ll force-pushed the chore/contact-suggestions-adr branch 2 times, most recently from 41da840 to b2bf2ad Compare September 17, 2026 09:19
@rezk2ll
rezk2ll force-pushed the chore/contact-suggestions-adr branch from b2bf2ad to a99b9d9 Compare September 18, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants