372 lines
13 KiB
Markdown
372 lines
13 KiB
Markdown
# 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](#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
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
# 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`:
|
|
|
|
```bash
|
|
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
|
|
|
|
Pushing a version tag (e.g. `v1.2.3`) or publishing a release triggers
|
|
[`.github/workflows/docker-release.yml`](.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, `<major>.<minor>`,
|
|
`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:
|
|
|
|
```yaml
|
|
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):
|
|
|
|
```bash
|
|
# 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](https://htmx.org/) 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](https://templ.guide/) for type-safe Go HTML
|
|
templates, [Tailwind CSS v4](https://tailwindcss.com/) 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
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.
|
|
|