arnefandCopilot 65cd24a192 Show plain calendar name for shared calendars, drop ~owner prefix
DAVx5/CalDAV clients displayed a shared calendar's DAV:displayname as
"alice~work" (the synthetic owner~name local identifier nidus uses
internally to keep a shared calendar's URL unique per subscriber).
That internal name is still needed for routing (it's still what appears
in the calendar's URL path), but there's no reason to show it to the
end user as the calendar's label.

calendarMeta() now always sets caldav.Calendar.Name to the plain
calendar name (e.g. "work"), while Path still uses the "owner~name"
local name. The web UI already showed the plain name via a separate
"shared by <owner>" entry, so it's unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 10:09:41 +02:00
wip
2026-04-23 21:56:59 +02:00
wip
2026-04-23 21:56:59 +02:00

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/caldav and /.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. Create your config.yaml

Copy the example config and edit it — config.yaml is git-ignored so your real settings never get committed:

cp config.example.yaml config.yaml

Users, calendars, and address books are no longer configured in config.yaml — they live in the SQLite database and are managed with nidusctl (see below).

3. Run the server

make run
# or
go run ./cmd/server -config config.yaml

The server starts at http://localhost:8080.

4. Create a user and their resources

go run ./tools/nidusctl -config config.yaml user create alice \
  --display-name "Alice Smith" --email alice@example.com
# (prompts for a password; use --password to skip the prompt, e.g. in scripts)

go run ./tools/nidusctl -config config.yaml calendar create alice personal
go run ./tools/nidusctl -config config.yaml addressbook create alice contacts

Users can also be created/removed via the web UI (/web/) once logged in as an existing user — see Web UI below.


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)

  1. Go to Settings → Calendar → Accounts → Add Account → Other → Add CalDAV Account
  2. Enter:
    • Server: http://yourserver:8080
    • Username: alice
    • Password: your plaintext password
  3. The app will auto-discover calendars at /cal/.

Same flow for CardDAV with Contacts app.

Thunderbird

  1. Install the TbSync add-on + CalDAV & CardDAV provider
  2. 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 home segment 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-set properties.


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 in nidus.db (web_sessions table), 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
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, calendars, and address books are managed via nidusctl, not config.yaml — see Managing users below.

Managing users

All user/calendar/address-book management is done with nidusctl (or the web UI). Nothing is stored in config.yaml anymore.

# Users
nidusctl user create <username> [--display-name NAME] [--email EMAIL] [--password PW]
nidusctl user delete <username>
nidusctl user list
nidusctl user passwd <username> [--password PW]

# Calendars
nidusctl calendar create <owner> <calendar> [--color '#RRGGBB']
nidusctl calendar color  <owner> <calendar> <hex-color>
nidusctl calendar delete <owner> <calendar>
nidusctl calendar list <owner>
nidusctl calendar share <owner> <calendar> <user> <read|write>
nidusctl calendar unshare <owner> <calendar> <user>
nidusctl calendar shares <owner> <calendar>

# Address books
nidusctl addressbook create <owner> <book>
nidusctl addressbook delete <owner> <book>
nidusctl addressbook list <owner>
nidusctl addressbook share <owner> <book> <user> <read|write>
nidusctl addressbook unshare <owner> <book> <user>
nidusctl addressbook shares <owner> <book>

Passwords are prompted for interactively (masked, double-entry) when --password is omitted. The web UI (/web/) also lets a logged-in user create/delete their own calendars and address books from the dashboard.

Upgrading from an older version? The users: section in config.yaml is no longer read. Recreate your users with nidusctl user create (and their calendars/address books) — there is no automatic migration from the old config format.


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
S
Description
No description provided
Readme AGPL-3.0
826 KiB
v2026.9.2
Latest
2026-09-08 20:21:01 +00:00
Languages
Go 82.1%
templ 12.8%
JavaScript 2.3%
TypeScript 2.1%
Makefile 0.4%
Other 0.3%