- List of the user's own calendars (web/calendar) - Month view per calendar with a Monday-first 6-week grid, showing all-day and timed events, colored dot matching the calendar's color - Navigation between months (prev/next/today) via query params - Create/edit/delete events: title, description, location, all-day toggle, start/end date+time - Clicking a day cell prefills a new all-day event on that date; the "New event" button defaults to a one-hour slot starting next full hour - Deleting an event or invalid time ranges (end before/equal start) are validated with inline error messages - Verified round-trip with the real CalDAV protocol handler (events created via the web UI are correctly visible to a REPORT query) 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. 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)
- 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, 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 inconfig.yamlis no longer read. Recreate your users withnidusctl 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 |