Documentation

Last updated: 2026-11-12

CardDAV behavior and storage reference

backend-contacts-http is a multi-instance CardDAV backend for Everlock.

Current state

  • multi-instance and multi-vhost
  • per-instance versioned-storage
  • Everlock-user HTTP authentication and http/contacts/<instance> access control
  • multiple address books per instance
  • plain persisted .vcf contacts, stored in vCard 4.0 and served in 3.0 or 4.0, with server-side create, update, and delete
  • CardDAV discovery, PROPFIND, PROPPATCH, GET, PUT, DELETE, MKCOL
  • conditional requests with ETags, checked again as the write is committed
  • REPORT sync-collection using real store commit ids as sync tokens

Shape

Each contacts instance has:

  • one instance name
  • one store
  • one or more vhosts
  • one CardDAV namespace served through frontend-http

Example config:

enabled = true

[instances.default]
store = "everlock-contacts"
vhosts = ["contacts.example.com"]

Stores and vhosts

store=<name> picks the store explicitly and is used as given — an existing store, a hosted git repository or a store another instance uses included — as long as it is a valid name: lowercase letters, digits and -, not first or last. Without store=, the store is the instance name, and /contacts create refuses it when a store or repository of that name already exists, another instance uses it, or Everlock reserves it (everlock-system, everlock-mail and every backend's bootstrap store such as calendar and everlock-contacts). The refusal says what the name collides with; pass store=<name> to use it on purpose, or pick another name.

Instances that share a store serve the same data: deleting an address book through one deletes it for all.

A vhost belongs to one contacts instance; /contacts create and /contacts set refuse a vhost another instance maps, comparing without regard to case. The creating user becomes Owner of the new instance. When two configured instances list one vhost, the first by name serves it and a warning is logged. A config file that is not TOML stops the backend from starting, while a single malformed instance entry is skipped with a warning.

CardDAV lives under /carddav, with discovery at /.well-known/carddav. An instance holds those two paths on each of its vhosts and nothing else, so a site, a file share or the calendar backend serves the rest of the same host; /contacts create and /contacts set refuse a vhost where another backend already holds one of the two paths. Virtual hosts explains how backends share a host.

.local vhosts are announced over mDNS as CardDAV services with path=/carddav as soon as an instance takes them on, and withdrawn when it gives them up.

A vhost inside a zone the DNS backend serves carries the RFC 6764 records clients look up when given just the vhost name: _carddavs._tcp.<vhost> SRV 0 1 <HTTPS port> <vhost>. with a TXT record path=/carddav beside it, and _carddav._tcp.<vhost> SRV 0 0 0 ., as CardDAV runs over HTTPS. The port is the one the HTTP frontend serves HTTPS on, and the records follow a vhost from the moment /contacts create or /contacts set saves it.

Storage

Contacts are stored as plain .vcf files in versioned-storage.

The current store layout is:

default/
  config.toml
  alice-smith.vcf
  alice-smith-2.vcf
work/
  config.toml
  vendor-x.vcf

Each contact is one .vcf file in vCard 4.0, named by the URL the client writes it to. A 4.0 card holds what the client sent, with CRLF line breaks and a UID added where one is missing. A 3.0 card — what Apple Contacts writes — is converted to 4.0 before it is stored. When the stored card differs from what was sent, the answer to the write leaves out the ETag, and the client fetches the stored version.

A UID is unique within an address book, and a contact keeps its UID: a write that gives an existing contact another one is refused with 403 no-uid-conflict. The check looks the UID up in a set of hashes Everlock keeps in memory per address book, so bulk uploads stay fast.

vCard 3.0 and 4.0

Clients read each contact in the version they ask for:

  • Apple Contacts asks for no version and gets vCard 3.0, which is the CardDAV default; so does a GET without an Accept header
  • DAVx⁵ asks for 4.0 and gets the stored card as it is
  • a GET with Accept: text/vcard; version=4.0 gets 4.0

Every answer names the version it carries in its content type, such as text/vcard; charset=utf-8; version=3.0. An address book listing gives each contact the size and type of the 3.0 card a plain GET returns.

The conversion maps what both versions have — preferred types, inline photos, Apple's groups, birthdays without a year, address labels, sort strings — and carries what only one of them has in X-EVERLOCK- properties and parameters, so a card goes from one version to the other and back with everything it held. Apple's own X- properties, such as custom labels, pass as they are. Every version of a contact carries the same ETag, so a client's next write finds the card it read. Address book queries filter the stored 4.0 card.

Address book properties

An address book's name and description live in its config.toml, and every other property a client sets on it is kept there as sent and returned when asked for; PROPPATCH with remove deletes one. Properties the server works out itself are refused with 403.

A PROPFIND gets the properties it names, and a 404 for each one the address book or contact lacks; a request for everything gets the full set.

Access model

The access path is:

http/contacts/<instance>

Role mapping:

  • Reader for discovery, listing, fetch, PROPFIND, and REPORT
  • Writer for PUT, DELETE, MKCOL, and PROPPATCH

The current model is instance-wide. There are no per-address-book ACLs yet.

CardDAV surface

Current URL shape:

/.well-known/carddav
/.well-known/carddav/
/carddav/
/carddav/principals/me/
/carddav/addressbooks/
/carddav/addressbooks/<book>/
/carddav/addressbooks/<book>/<contact>.vcf

Supported today:

  • discovery and PROPFIND
  • contact GET / HEAD
  • REPORT addressbook-multiget
  • REPORT addressbook-query, with property and parameter filters, match types and negation
  • REPORT sync-collection
  • writable PUT, DELETE, and MKCOL, an extended MKCOL setting the new address book's properties
  • PROPPATCH setting and removing address book properties

Sync

Address-book sync tokens use the real versioned-storage commit id.

That means incremental sync is derived directly from store history instead of a second sync database. The token a sync hands out stands for exactly the contacts it reports, and a token the address book cannot sync from makes the client start over with a full sync.

contacts backend carddav