arnefandCopilot 28c55804d8 Fix unreadable event text when a calendar's color is white/very light
The month view rendered each event's text (and its little leading dot)
in the calendar's own color, on a very lightly tinted (~13% opacity)
background of that same color. For a light color like white or pale
yellow, the text ended up effectively invisible against its own
near-white background.

internal/web/templates/calendar.templ: add eventTextColor(hex), which
darkens colors above a perceived-luminance threshold (keeping the hue,
e.g. white -> a mid gray, pale yellow -> olive) while leaving already-
legible colors untouched; used for both the event text and its leading
dot. Also add a subtle ring to the event dot and to the calendar legend's
color swatch so a white/near-white swatch stays visible against the
page's white background, independent of the text-color fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 23:27:19 +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%