diff --git a/docs/plans/2026-01-12-carddav-architecture.md b/docs/plans/2026-01-12-carddav-architecture.md
new file mode 100644
index 0000000..18ca8b3
--- /dev/null
+++ b/docs/plans/2026-01-12-carddav-architecture.md
@@ -0,0 +1,173 @@
+# CardDAV Server Architecture
+
+Phase 1: Read-only CardDAV server for Apple Contacts.
+
+## RFCs
+
+| RFC | Name | Purpose |
+|-----|------|---------|
+| [RFC 4918](https://datatracker.ietf.org/doc/html/rfc4918) | WebDAV | HTTP extensions for distributed authoring |
+| [RFC 6352](https://datatracker.ietf.org/doc/html/rfc6352) | CardDAV | vCard extensions to WebDAV |
+| [RFC 6350](https://datatracker.ietf.org/doc/html/rfc6350) | vCard 4.0 | Contact data format |
+| [RFC 3253](https://datatracker.ietf.org/doc/html/rfc3253) | REPORT | Required for addressbook-multiget |
+
+## HTTP Methods
+
+### Phase 1 (read-only)
+
+| Method | Purpose |
+|--------|---------|
+| `OPTIONS` | Advertise capabilities |
+| `PROPFIND` | Discovery + collection listing |
+| `GET` | Retrieve individual vCards |
+| `REPORT` | addressbook-multiget (batch fetch) |
+
+### Deferred
+
+| Method | Purpose |
+|--------|---------|
+| `PUT` | Create/update vCards |
+| `DELETE` | Remove vCards |
+| `MKCOL` | Create address books |
+| `PROPPATCH` | Modify properties |
+| `addressbook-query` | Search/filter contacts |
+
+## URL Structure
+
+```
+/.well-known/carddav → redirect to /principal/
+/principal/ → user's principal resource
+/addressbooks/ → addressbook-home-set
+/addressbooks/contacts/ → the address book collection
+/addressbooks/contacts/{id}.vcf → individual vCard
+```
+
+User identity from auth, not URL path.
+
+## Discovery Flow
+
+```
+1. OPTIONS /.well-known/carddav
+ → 301 Redirect to /principal/
+
+2. OPTIONS /principal/
+ → 200 OK
+ → DAV: 1, 3, addressbook
+
+3. PROPFIND /principal/ (Depth: 0)
+ → current-user-principal: /principal/
+ → addressbook-home-set: /addressbooks/
+
+4. PROPFIND /addressbooks/ (Depth: 1)
+ → Lists /addressbooks/contacts/ as type addressbook
+
+5. REPORT /addressbooks/contacts/ (addressbook-multiget)
+ → Returns vCards
+```
+
+### Key Properties
+
+| Property | Location | Value |
+|----------|----------|-------|
+| `current-user-principal` | `/principal/` | `/principal/` |
+| `addressbook-home-set` | `/principal/` | `/addressbooks/` |
+| `resourcetype` | `/addressbooks/contacts/` | collection, addressbook |
+| `displayname` | `/addressbooks/contacts/` | "Contacts" |
+| `getctag` | `/addressbooks/contacts/` | change tag for sync |
+
+## Storage
+
+### Directory Structure
+
+```
+data/
+└── contacts/
+ ├── kqmtnwpxlrvszoyp.kdl
+ ├── nzxwvtslqpomkrny.kdl
+ └── plokmnzxwvtsrqky.kdl
+```
+
+### Contact File Format (KDL)
+
+```kdl
+contact {
+ name "John Smith"
+ phone "+1-555-1234" type="mobile"
+ email "john@example.com"
+}
+```
+
+### Contact IDs
+
+- 16 characters using k-z (16 letters)
+- 64 bits of entropy
+- Filename is the ID: `{id}.kdl`
+- Maps to vCard UID
+
+### Change Detection
+
+- `getctag`: hash of directory listing or newest mtime
+- `getetag`: mtime or content hash of .kdl file
+
+### Deferred
+
+- Per-user addressbook files (`addressbooks/{user}.kdl`)
+- Groups with shared attributes
+- Database migration
+
+## Roda Architecture
+
+### File Structure
+
+```
+lib/
+└── pro_tacts/
+ └── app.rb
+```
+
+Extract classes when needed.
+
+### Routing
+
+```ruby
+module ProTacts
+ class App < Roda
+ plugin :all_verbs
+
+ route do |r|
+ r.on ".well-known/carddav" do
+ r.redirect "/principal/"
+ end
+
+ r.on "principal" do
+ r.is do
+ r.options { dav_options }
+ r.propfind { principal_propfind(r) }
+ end
+ end
+
+ r.on "addressbooks" do
+ r.is do
+ r.propfind { home_propfind(r) }
+ end
+
+ r.on "contacts" do
+ r.is do
+ r.options { dav_options }
+ r.propfind { collection_propfind(r) }
+ r.report { addressbook_multiget(r) }
+ end
+
+ r.on String do |id|
+ r.get { serve_vcard(id) }
+ end
+ end
+ end
+ end
+ end
+end
+```
+
+## Translation
+
+KDL → vCard on read. Server parses KDL contact files and generates vCard 4.0 output.
diff --git a/docs/plans/2026-01-12-carddav-reference.md b/docs/plans/2026-01-12-carddav-reference.md
new file mode 100644
index 0000000..2aee9bf
--- /dev/null
+++ b/docs/plans/2026-01-12-carddav-reference.md
@@ -0,0 +1,295 @@
+# CardDAV Protocol Reference
+
+Examples and formats from the RFCs for implementation reference.
+
+## XML Namespaces
+
+```xml
+xmlns:D="DAV:"
+xmlns:C="urn:ietf:params:xml:ns:carddav"
+```
+
+## OPTIONS
+
+**Request:**
+```http
+OPTIONS /principal/ HTTP/1.1
+Host: carddav.example.com
+```
+
+**Response:**
+```http
+HTTP/1.1 200 OK
+DAV: 1, 3, addressbook
+Allow: OPTIONS, GET, PROPFIND, REPORT
+```
+
+## PROPFIND
+
+### Request Format
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:propfind xmlns:D="DAV:">
+ <D:prop>
+ <D:displayname/>
+ <D:resourcetype/>
+ </D:prop>
+</D:propfind>
+```
+
+### Request with CardDAV Properties
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+ <D:prop>
+ <D:current-user-principal/>
+ <C:addressbook-home-set/>
+ </D:prop>
+</D:propfind>
+```
+
+### 207 Multi-Status Response
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>/principal/</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:displayname>Alice</D:displayname>
+ <D:resourcetype/>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>
+```
+
+### Response with Multiple Properties (some missing)
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>/principal/</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:displayname>Alice</D:displayname>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ <D:propstat>
+ <D:prop>
+ <D:getcontentlength/>
+ </D:prop>
+ <D:status>HTTP/1.1 404 Not Found</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>
+```
+
+## Discovery Properties
+
+### current-user-principal
+
+```xml
+<D:current-user-principal xmlns:D="DAV:">
+ <D:href>/principal/</D:href>
+</D:current-user-principal>
+```
+
+### addressbook-home-set
+
+```xml
+<C:addressbook-home-set xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+ <D:href>/addressbooks/</D:href>
+</C:addressbook-home-set>
+```
+
+### resourcetype (for address book collection)
+
+```xml
+<D:resourcetype xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+ <D:collection/>
+ <C:addressbook/>
+</D:resourcetype>
+```
+
+### supported-address-data
+
+```xml
+<C:supported-address-data xmlns:C="urn:ietf:params:xml:ns:carddav">
+ <C:address-data-type content-type="text/vcard" version="4.0"/>
+</C:supported-address-data>
+```
+
+## REPORT: addressbook-multiget
+
+### Request
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+ <D:prop>
+ <D:getetag/>
+ <C:address-data/>
+ </D:prop>
+ <D:href>/addressbooks/contacts/kqmtnwpxlrvszoyp.vcf</D:href>
+ <D:href>/addressbooks/contacts/nzxwvtslqpomkrny.vcf</D:href>
+</C:addressbook-multiget>
+```
+
+### Request with Specific vCard Properties
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+ <D:prop>
+ <D:getetag/>
+ <C:address-data>
+ <C:prop name="VERSION"/>
+ <C:prop name="UID"/>
+ <C:prop name="FN"/>
+ <C:prop name="EMAIL"/>
+ </C:address-data>
+ </D:prop>
+ <D:href>/addressbooks/contacts/kqmtnwpxlrvszoyp.vcf</D:href>
+</C:addressbook-multiget>
+```
+
+### Response
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+ <D:response>
+ <D:href>/addressbooks/contacts/kqmtnwpxlrvszoyp.vcf</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:getetag>"abc123"</D:getetag>
+ <C:address-data>BEGIN:VCARD
+VERSION:4.0
+UID:kqmtnwpxlrvszoyp
+FN:John Smith
+EMAIL:john@example.com
+END:VCARD
+</C:address-data>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>
+```
+
+## vCard 4.0 Format
+
+### Minimal vCard
+
+```
+BEGIN:VCARD
+VERSION:4.0
+UID:kqmtnwpxlrvszoyp
+FN:John Smith
+END:VCARD
+```
+
+### Full Example
+
+```
+BEGIN:VCARD
+VERSION:4.0
+UID:kqmtnwpxlrvszoyp
+FN:John Smith
+N:Smith;John;;;
+TEL;TYPE=mobile:+1-555-1234
+TEL;TYPE=work:+1-555-5678
+EMAIL;TYPE=home:john@example.com
+EMAIL;TYPE=work:jsmith@work.com
+ADR;TYPE=home:;;123 Main St;Springfield;IL;62701;USA
+END:VCARD
+```
+
+### Property Reference
+
+| Property | Required | Format |
+|----------|----------|--------|
+| `BEGIN` | Yes | `BEGIN:VCARD` |
+| `VERSION` | Yes | `VERSION:4.0` |
+| `UID` | Yes | Unique identifier |
+| `FN` | Yes | Formatted name (display name) |
+| `END` | Yes | `END:VCARD` |
+| `N` | No | `family;given;additional;prefix;suffix` |
+| `TEL` | No | Phone with optional TYPE |
+| `EMAIL` | No | Email with optional TYPE |
+| `ADR` | No | `pobox;ext;street;city;region;postal;country` |
+
+### TYPE Values
+
+- TEL: `work`, `home`, `mobile`, `fax`, `pager`
+- EMAIL: `work`, `home`
+- ADR: `work`, `home`
+
+## KDL to vCard Mapping
+
+### KDL Input
+
+```kdl
+contact {
+ name "John Smith"
+ phone "+1-555-1234" type="mobile"
+ phone "+1-555-5678" type="work"
+ email "john@example.com" type="home"
+ address type="home" {
+ street "123 Main St"
+ city "Springfield"
+ state "IL"
+ zip "62701"
+ country "USA"
+ }
+}
+```
+
+### vCard Output
+
+```
+BEGIN:VCARD
+VERSION:4.0
+UID:kqmtnwpxlrvszoyp
+FN:John Smith
+TEL;TYPE=mobile:+1-555-1234
+TEL;TYPE=work:+1-555-5678
+EMAIL;TYPE=home:john@example.com
+ADR;TYPE=home:;;123 Main St;Springfield;IL;62701;USA
+END:VCARD
+```
+
+## HTTP Headers
+
+### Request Headers
+
+| Header | Value | When |
+|--------|-------|------|
+| `Depth` | `0`, `1`, `infinity` | PROPFIND |
+| `Content-Type` | `application/xml; charset=utf-8` | PROPFIND, REPORT |
+
+### Response Headers
+
+| Header | Value | When |
+|--------|-------|------|
+| `DAV` | `1, 3, addressbook` | OPTIONS |
+| `Content-Type` | `application/xml; charset=utf-8` | 207 responses |
+| `Content-Type` | `text/vcard; charset=utf-8` | GET vCard |
+| `ETag` | `"hash"` | GET, in propstat |
+
+## Status Codes
+
+| Code | Meaning |
+|------|---------|
+| 200 | OK |
+| 207 | Multi-Status (contains per-resource status) |
+| 301 | Redirect (for .well-known) |
+| 404 | Not Found |
+| 405 | Method Not Allowed |