Files
nidus/README.md
T

373 lines
14 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
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
[`.gitea/workflows/docker-release.yml`](.gitea/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 — configuration is done entirely via environment
variables (no `config.yaml` to copy over):
```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 any config file) 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 calendar share alice work bob write
# List everyone alice's "work" calendar is shared with
go run ./tools/nidusctl calendar shares alice work
# Revoke access
go run ./tools/nidusctl calendar unshare alice work bob
# Address books work the same way, using "addressbook" instead of "calendar"
go run ./tools/nidusctl addressbook share alice contacts bob read
```
(Both the server and `nidusctl` resolve the data directory from the
`NIDUS_DATA_DIR` environment variable.)
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, sort by name or
date, 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 (first/last name, organization, birthday, phone numbers,
emails, address, 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), with a detail view for each event; 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/`). User and resource data lives in `nidus.db`, not in a
config file.
```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>
```
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, address books, and ICS/webcal
subscriptions from the dashboard.
---
## Project layout
```
nidus/
├── cmd/server/ # main entrypoint
├── internal/
│ ├── auth/ # HTTP Basic Auth middleware (DAV endpoints)
│ ├── birthdays/ # compute a virtual Birthday calendar from contacts
│ ├── caldav/ # CalDAV backend (incl. ICS subscriptions & Birthdays)
│ ├── carddav/ # CardDAV backend
│ ├── config/ # configuration loader (environment variables)
│ ├── db/ # SQLite store (users, calendars, shares, sessions)
│ ├── icalfix/ # iCalendar (RFC 5545) parsing/fixing helpers
│ ├── icssub/ # remote ICS/webcal subscription fetcher
│ ├── store/ # filesystem storage layer
│ ├── web/ # web UI (templ, dashboard, share mgmt, sessions)
│ │ └── templates/ # templ templates (+ generated *_templ.go)
│ └── webdav/ # WebDAV file handler
├── tools/nidusctl/ # admin CLI (users, calendars, address books, sharing)
├── tools/migrate/ # data directory migration tool
├── tools/hashpwd/ # standalone bcrypt password generator
├── web/ # front-end assets: Tailwind input/config, static/
│ └── static/ # compiled app.css + htmx.min.js (embedded into the binary)
├── 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.