- web/ts/login.ts: small vanilla TS module toggling the password input's type between 'password'/'text' via a 'Show/Hide' button, so users can rule out typos before submitting. Compiled to web/static/login.js (ES module) via tsc (web/tsconfig.json, new 'make web-ts'/'web-assets' Makefile targets) and loaded via <script type="module">. Compiled JS is committed/embedded the same way as the compiled CSS — no Node.js needed at runtime. - Verified end-to-end against the live proxied server (https://local.unqr.dev/web/login): POST /web/login correctly returns 303 + Set-Cookie, and the session then authorizes GET /web/ — the server-side login flow is confirmed working correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DAV Server
A self-hosted CalDAV, CardDAV, and WebDAV server written in Go.
Features
| Protocol | Use case |
|---|---|
| CalDAV | Calendars — sync with Apple Calendar, Thunderbird, GNOME Calendar, … |
| CardDAV | Contacts — sync with Apple Contacts, GNOME Contacts, … |
| WebDAV | General file access via Finder, Windows Explorer, Nautilus, … |
- HTTP Basic Auth with bcrypt password storage
- Per-user isolated collections
- Calendar/address book sharing — grant other users read or write access to your calendars/address books
- Web UI — a small dashboard (login, manage shares) at
/web/, built with templ + Tailwind + htmx - Auto-discovery via
/.well-known/caldavand/.well-known/carddav - Optional TLS (or use a reverse proxy)
- Structured logging (text or JSON)
- Graceful shutdown
- Docker & Docker Compose support
Quick start
1. Install dependencies
go mod tidy
2. Generate password hashes
go run ./tools/hashpwd mysecretpassword
# Outputs: $2b$12$...
3. Create your config.yaml
Copy the example config and edit it — config.yaml is git-ignored so your
real credentials/domain never get committed:
cp config.example.yaml config.yaml
Replace the placeholder hashes with your real bcrypt hashes:
users:
alice:
password: "$2b$12$<hash generated above>"
display_name: "Alice Smith"
email: "alice@example.com"
calendars:
- personal
- work
address_books:
- contacts
4. Run the server
make run
# or
go run ./cmd/server -config config.yaml
The server starts at http://localhost:8080.
Docker
# Build and start
docker compose up --build
# Or build manually
docker build -t davserver .
docker run -p 8080:8080 \
-v ./config.yaml:/app/config.yaml:ro \
-v dav-data:/app/data \
davserver
Client configuration
Apple Calendar / Contacts (macOS / iOS)
- Go to Settings → Calendar → Accounts → Add Account → Other → Add CalDAV Account
- Enter:
- Server:
http://yourserver:8080 - Username:
alice - Password: your plaintext password
- Server:
- The app will auto-discover calendars at
/cal/.
Same flow for CardDAV with Contacts app.
Thunderbird
- Install the TbSync add-on + CalDAV & CardDAV provider
- Add a new account and point it at
http://yourserver:8080/.well-known/caldav
GNOME Calendar / Evolution
Use the GNOME Online Accounts panel:
- Server:
http://yourserver:8080 - Check CalDAV / CardDAV as appropriate
API endpoints
| Path | Description |
|---|---|
/.well-known/caldav |
Redirects to /cal/ |
/.well-known/carddav |
Redirects to /card/ |
/cal/ |
CalDAV principal (same URL for every user; resolved via Basic Auth) |
/cal/home/ |
Calendar home-set (lists the user's calendars) |
/cal/home/<calendar>/ |
Calendar collection |
/card/ |
CardDAV principal (same URL for every user; resolved via Basic Auth) |
/card/home/ |
Address book home-set (lists the user's address books) |
/card/home/<book>/ |
Address book collection |
/files/ |
WebDAV file storage (same URL for every user; resolved via Basic Auth) |
/healthz |
Health check (unauthenticated) |
Note: the
homesegment is a fixed literal (not a username or real resource) — it exists only to give the calendar/address-book home-set the path depth that the underlying CalDAV/CardDAV library expects when classifying resources by URL. Clients should never need to construct these URLs by hand; they're discovered automatically via.well-known+current-user-principal+calendar-home-set/addressbook-home-setproperties.
Sharing calendars and address books
A user can grant another user read or write access to one of their own calendars or address books. Shared resources show up automatically in the grantee's own home-set alongside their own calendars — no separate account or extra client configuration needed.
Sharing grants are stored in a small SQLite database at
<data_dir>/nidus.db (not in config.yaml) and can be managed either via
the nidusctl CLI or the web UI's dashboard (see below):
# Give bob write access to alice's "work" calendar
go run ./tools/nidusctl -config config.yaml calendar share alice work bob write
# List everyone alice's "work" calendar is shared with
go run ./tools/nidusctl -config config.yaml calendar shares alice work
# Revoke access
go run ./tools/nidusctl -config config.yaml calendar unshare alice work bob
# Address books work the same way, using "addressbook" instead of "calendar"
go run ./tools/nidusctl -config config.yaml addressbook share alice contacts bob read
Or via make: make nidusctl ARGS="calendar share alice work bob write".
A calendar that alice shares with bob appears in bob's calendar
home-set as /cal/home/alice~work/ (i.e. <owner>~<calendar name>) — the
data itself still physically lives under alice's own storage; nothing is
copied. The same scheme applies to address books under /card/home/.
Read-only shares reject any write (PUT/DELETE) with 403 Forbidden.
Web UI
A small server-rendered dashboard is served at /web/ (separate from the
DAV endpoints, which stay on HTTP Basic Auth):
- Login (
/web/login) — cookie-based session, stored server-side innidus.db(web_sessionstable), independent of DAV Basic Auth. Includes a "Show/Hide" password toggle to rule out typos before submitting. - Dashboard (
/web/) — lists your own calendars/address books, who they're shared with, and any resources other users have shared with you. - Share management — add/remove shares directly from the dashboard
(same effect as
nidusctl); updates happen in place via htmx without a full page reload. - Logout (
/web/logout).
Implementation: templ for type-safe Go HTML
templates, Tailwind CSS v4 for styling, htmx
for the sprinkles of dynamic behavior (form submission via POST/DELETE,
partial page swaps), and TypeScript (compiled to plain JS, web/ts/) for
the few bits of client-side-only logic (e.g. the password-visibility
toggle) — no separate JS framework needed. The compiled CSS, compiled JS,
and the htmx bundle are all embedded into the Go binary
(web/staticassets.go), so no Node.js is required at runtime, only when
you change styles, templates, or TypeScript during development:
make web-deps # once, installs the Tailwind CLI + TypeScript compiler (needs Node.js/npm)
make web-assets # regenerate templ code + rebuild web/static/app.css and web/static/*.js
TLS / Reverse proxy
Self-signed certificate (development)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
Update config.yaml:
tls:
enabled: true
cert_file: cert.pem
key_file: key.pem
Caddy reverse proxy (recommended for production)
dav.example.com {
reverse_proxy localhost:8080
}
Configuration reference
server:
host: "0.0.0.0"
port: 8080
base_url: "https://dav.example.com" # used in DAV responses
auth:
realm: "My DAV Server"
storage:
data_dir: "./data" # all user data lives here
logging:
level: "info" # debug | info | warn | error
format: "text" # text | json
tls:
enabled: false
cert_file: ""
key_file: ""
users:
<username>:
password: "<bcrypt hash>"
display_name: "Full Name"
email: "user@example.com"
calendars: # pre-created calendar names
- personal
address_books: # pre-created address book names
- contacts
Project layout
caldav-server/
├── cmd/server/ # main entrypoint
├── internal/
│ ├── auth/ # HTTP Basic Auth middleware (DAV endpoints)
│ ├── caldav/ # CalDAV backend
│ ├── carddav/ # CardDAV backend
│ ├── config/ # YAML config loader
│ ├── db/ # SQLite store (shares, web UI sessions)
│ ├── store/ # filesystem storage layer
│ ├── web/ # web UI (cookie sessions, dashboard, share mgmt)
│ │ └── templates/ # templ templates (+ generated *_templ.go)
│ └── webdav/ # WebDAV file handler
├── tools/hashpwd/ # bcrypt password hasher CLI
├── tools/nidusctl/ # sharing-grant admin CLI
├── web/ # front-end assets: Tailwind input/config, static/
│ └── static/ # compiled app.css + htmx.min.js (embedded into the binary)
├── config.example.yaml # sample configuration (copy to config.yaml)
├── Dockerfile
├── docker-compose.yaml
└── Makefile
Running tests
make test
# or
go test ./... -race
Dependencies
| Package | Purpose |
|---|---|
github.com/emersion/go-webdav |
WebDAV/CalDAV/CardDAV protocol layer |
github.com/emersion/go-ical |
iCalendar parsing/serialisation |
github.com/emersion/go-vcard |
vCard parsing/serialisation |
golang.org/x/crypto |
bcrypt |
golang.org/x/net |
golang.org/x/net/webdav |
gopkg.in/yaml.v3 |
YAML config parsing |
modernc.org/sqlite |
Pure-Go SQLite driver (shares, web UI sessions) |
github.com/a-h/templ |
Type-safe Go HTML templates (web UI) |
Front-end (dev-only, not required at runtime — see Web UI):
| Tool | Purpose |
|---|---|
Tailwind CSS v4 (web/package.json) |
Utility-first CSS, compiled to web/static/app.css |
htmx (web/static/htmx.min.js, vendored) |
Partial page updates without a JS framework |