Files
nidus/README.md
T

14 KiB

nidus

A self-hosted CalDAV, CardDAV, and WebDAV server written in Go.

About the name

The project is called nidus — Latin for "nest". Like a nest, it's a small, self-hosted, personal home for your own data: calendars, contacts, and files, all kept under your own roof instead of a third-party cloud.

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 mobile-friendly app at /web/ for managing calendars, contacts, files, and account settings (see Web UI below), built with templ + Tailwind + htmx
  • ICSSubscriptions — add remote ICS/webcal calendars
  • Birthdays calendar — auto-computed from contacts' BDAY fields
  • Auto-discovery via /.well-known/caldav and /.well-known/carddav
  • Structured logging (text or JSON)
  • Graceful shutdown
  • Docker & Docker Compose support

Quick start

1. Run the server

make run
# or
go run ./cmd/server

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

2. Configure the server via environment variables

The server is configured via environment variables:

# Required: Set the data directory
export NIDUS_DATA_DIR="./data"

# Optional: Set port, host, and base URL
export NIDUS_PORT="8080"
export NIDUS_HOST="0.0.0.0"
export NIDUS_BASE_URL="https://dav.example.com"

# Optional: Set auth realm
export NIDUS_AUTH_REALM="My DAV Server"

# Optional: Set logging
export NIDUS_LOG_LEVEL="info"
export NIDUS_LOG_FORMAT="text"

This server does not handle TLS — use a reverse proxy (e.g. Nginx, Caddy, Traefik) to terminate TLS and forward requests to the server.

3. Create a user and their resources

go run ./tools/nidusctl 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 calendar create alice personal
go run ./tools/nidusctl addressbook create alice contacts

Or use the web UI (/web/) once logged in — see Web UI below.


Docker

# Build and start
docker compose up --build

# Or build manually
docker build -t nidus .
docker run -p 8080:8080 \
  -v nidus-data:/app/data \
  -e NIDUS_DATA_DIR=/app/data \
  nidus

The image also ships nidusctl, so once the container is running you can create your first user (and their calendars/address books) with docker compose exec:

docker compose exec nidus nidusctl user create alice \
  --display-name "Alice Smith" --email alice@example.com
# (prompts for a password; use --password to skip the prompt)

docker compose exec nidus nidusctl calendar create alice personal
docker compose exec nidus nidusctl addressbook create alice contacts

Using pre-built images

This project uses year.month.hotfix versioning (e.g. 2026.8.0, 2026.8.1) rather than semantic versioning. Pushing a version tag (e.g. 2026.8.0) or publishing a release triggers .github/workflows/docker-release.yml, which builds and publishes a multi-arch (linux/amd64 + linux/arm64) image to git.arnef.de/arnef/nidus, tagged with the version, <year>.<month>, latest, and the short commit SHA. It authenticates via the REGISTRY_USERNAME/REGISTRY_PASSWORD repository secrets.

A minimal standalone docker-compose.yml that pulls this image instead of building from a checkout — just fetch config.example.yaml, copy it to config.yaml, and adjust it to your needs:

services:
  nidus:
    image: git.arnef.de/arnef/nidus:latest
    ports:
      - "8080:8080"
    volumes:
      - nidus-data:/app/data
    environment:
      - NIDUS_DATA_DIR=/app/data
      # Optional: other environment variables
      # - NIDUS_PORT=8080
      # - NIDUS_HOST=0.0.0.0
      # - NIDUS_BASE_URL=https://dav.example.com
      # - NIDUS_AUTH_REALM="My DAV Server"
      # - NIDUS_LOG_LEVEL=info
      # - NIDUS_LOG_FORMAT=text
    restart: unless-stopped

volumes:
  nidus-data:

Note: This server does not handle TLS. Use a reverse proxy (e.g. Nginx, Caddy, Traefik) to terminate TLS and forward requests to the server.

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 app is served at /web/ (separate from the DAV endpoints, which stay on HTTP Basic Auth), and works on both desktop and mobile browsers:

  • 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/) — create/delete your own calendars, address books, ICS/webcal subscriptions, and the Birthdays calendar; see who your resources are shared with and what others have shared with you; manage sharing grants directly (same effect as nidusctl) — updates happen in place via htmx without a full page reload.
  • Files (/web/files/) — a browser for the same storage the WebDAV endpoint (/files/) serves: navigate folders, create new folders, upload files/folders (including via drag & drop), download, and delete files or folders. Files open inline in the browser when the type supports it (video, audio, images, PDF, …) instead of always forcing a download; a separate "Download" action is always available to force a save-as.
  • Contacts (/web/contacts/) — browse address books, create/edit/ delete contacts (name, organization, birthday, phone numbers, emails, addresses, photo), and import/export vCards (.vcf).
  • Calendar (/web/calendar) — month and week views across all your own and shared calendars (including ICS/webcal subscriptions and the Birthdays calendar), create/edit/delete events, per-calendar colors, and import/export .ics files.
  • Account (/web/account) — update your display name/email and change your password.
  • 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, file drag & drop) — 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

Configuration reference

Configuration is done via environment variables:

Variable Default Description
NIDUS_HOST 0.0.0.0 Server listen host
NIDUS_PORT 8080 Server listen port
NIDUS_BASE_URL (auto) Public URL for DAV responses (e.g. https://dav.example.com)
NIDUS_AUTH_REALM DAV Server HTTP Basic Auth realm
NIDUS_DATA_DIR ./data Data directory for all user data
NIDUS_LOG_LEVEL info Log level: debug, info, warn, error
NIDUS_LOG_FORMAT text Log format: text, json

Example:

export NIDUS_DATA_DIR="./data"
export NIDUS_PORT="8080"
export NIDUS_BASE_URL="https://dav.example.com"
export NIDUS_LOG_LEVEL="info"

This server does not handle TLS — use a reverse proxy (e.g. Nginx, Caddy, Traefik) to terminate TLS and forward requests to the server.

Managing users

All user/calendar/address-book management is done with nidusctl or the web UI (/web/). 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>

Password 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, address books, and ICS/webcal subscriptions 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

nidus/
├── 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 (users, calendars, shares, sessions)
│   ├── store/           # filesystem storage layer
│   └── webdav/          # WebDAV file handler
├── internal/web/        # web UI (templ, dashboard, share mgmt, sessions)
│   └── templates/       # templ templates (+ generated *_templ.go)
├── cmd/nidusctl/        # admin CLI (users, calendars, address books, sharing)
├── web/                 # front-end assets: Tailwind input/config, static/
│   └── static/          # compiled app.css + htmx.min.js (embedded into the binary)
├── tools/migrate/       # data directory migration tool
├── config.example.yaml  # sample configuration (copy to config.yaml)
├── Dockerfile
├── docker-compose.yaml
└── Makefile

Running tests

make test
# or
go test ./... -race

A note on AI assistance

Large parts of this project's code and documentation were written with the help of AI coding assistants (e.g. GitHub Copilot). Changes are reviewed and tested where practical, but not every part of the codebase has been fully reviewed yet — use accordingly, especially before relying on this in security-sensitive environments.