Self-hostable web client for your CalDAV calendars and tasks that actually looks good.
Tested with Nextcloud, made for any CalDAV server.
Features · Status · Quick start · Configuration · Security · Contributing
Lucid gives the CalDAV server you already have a calendar you enjoy opening: the views and drag & drop you know from Google Calendar, tasks with checklists, light and dark mode. Your data stays where it is. There is nothing to migrate and no database to run, just a single binary or container in front of your server.
Lucid is also a contribution to DI.DAY, the Digital Independence Day, which invites you to swap one Big Tech service for a privacy-friendly alternative on the first Sunday of every month. Your calendar can be one of them: move it to your own CalDAV server or a provider you trust, and Lucid keeps the comfort you're used to from Google Calendar.
- Calendar views: month, week, day and agenda, with a color and a show/hide toggle for every calendar.
- Drag & drop: move events and tasks and resize events right in the calendar, a new one too while you are still naming it; changes show up instantly.
- Tasks: a task sidebar that groups each list by when its tasks are due, with checklists, start and due dates, priorities, repeats and completion. Rename a task or move it to another day right in the list, and delete done tasks one by one or all at once. Tasks with a date also appear in the calendar views, repeating ones at every repeat, where you can check them off or hide them once they're done. Checklists are saved as plain Markdown task lines, so your other apps can still read them.
- Recurring events and time zones: recurring series, all-day and timed events, shown in your local time zone in the calendar and the lists, and time zones of their own for the events and tasks that need them, such as a flight, which their editor and details show too.
- Custom repeats: beyond daily, weekly, monthly and yearly, the repeat editor builds repeats of events and tasks such as "every 2 weeks on Monday and Thursday" or "every month on the last weekday", with an end date or a number of times. It shows the repeat as a sentence and lists its next dates; everywhere else, repeats are shown in words too.
- Easy login: enter just your domain; Lucid finds the CalDAV endpoint via
/.well-known/caldavand DNS SRV records. - Fast: repeated requests are answered from a short-lived cache that
checks your server for changes. Changed something on your phone? Refresh
with a click or
rto see it right away. - Yours to adjust: light and dark mode, English and German, date and time formats that follow your locale or your own choice in the settings.
- Keyboard friendly: shortcuts and full keyboard navigation (WCAG 2.1 AA target).
- Easy to run: download it and double-click on your own computer, or run a single static binary, a distroless container or a NixOS module on a server, with JSON logs, Prometheus metrics and health endpoints. NixOS and Home Manager modules cover both.
Lucid is young: this is its first release series (0.x), made for early adopters. It reads and writes your real calendars, so try it with a test calendar first. Until 1.0, a release may change settings or behavior; its release notes say what changed.
Tested with Nextcloud. Lucid speaks plain CalDAV, so other servers such as Radicale, Baïkal, SOGo or Synology should work, but none has been tried yet. If yours doesn't, please report it: the problem report is made for exactly that. Lucid signs in with a user name and password; iCloud needs an app-specific password, and Google Calendar, which only offers OAuth, doesn't work.
Not there yet:
- Reminders: Lucid doesn't show or set alarms. Alarms set in other apps stay when you edit an event or task in Lucid.
- Invitations: attendees aren't shown, and you can't answer an invitation. They stay when you edit, but your server may email them: Nextcloud tells the attendees when the organizer changes an event's time or repeat or deletes it, and the organizer when an attendee deletes it, though not for a new title, place or note, nor for past events.
- Calendars: no creating, renaming or deleting calendars, no
subscriptions to calendar feeds (webcal), no import or export of
.icsfiles, and one account at a time. Calendars shared with you show if your server puts them in your calendar home, as Nextcloud does. - Events and tasks: no search, no moving to another calendar, and no subtasks.
- Touch screens: drag and drop is made for mouse and keyboard and untested on touch screens; the editor works everywhere.
- Sessions live in memory only, so restarting Lucid signs everybody out.
![]() |
![]() |
| Tasks right in the calendar | Tasks with checklists |
![]() |
![]() |
| Week view in dark mode | Event details |
![]() |
![]() |
| Create an event or a task with a click | Sign in with just your domain |
flowchart LR
browser["Your browser"] -- "HTTP(S) + JSON" --> lucid["Lucid"]
lucid -- "CalDAV" --> server["Your CalDAV server"]
Lucid is a smart proxy: a single Go binary with the web app embedded. It talks JSON to your browser and CalDAV to your server. Your CalDAV server stays the single source of truth; Lucid only keeps sessions and a short-lived cache in memory. After login, your browser holds nothing but a session cookie, never your CalDAV credentials.
A repeating task shows up at every repeat in the calendar views. Checking off the current repeat moves the task on to its next one and leaves the done one behind as a completed entry of its own. Repeats are checked off in order. Where there is a choice, a change of a repeat asks which repeats it reaches: only the current one, this one and the following ones, or all of them. Apple Reminders and Tasks.org roll a repeating task forward the same way, so they see what you did in Lucid, and Lucid reads what Thunderbird, KDE and others write. docs/RECURRING-TASKS.md explains why, with a survey of how CalDAV clients handle repeating tasks.
The calendar, the task list and the details show every time in your
browser's time zone, and the editor and the details also show an event or
task in time zones of its own, if it has any (see below). A repeating event
or task you create in Lucid gets your browser's time zone, such as
Europe/Berlin, as calendar apps usually do: that is what keeps a weekly
meeting at 9:00 when daylight saving time begins or ends. A time zone stays
with its event or task on your CalDAV server, where whoever runs the server,
and whoever you share the calendar with, can see it, so a single event or
task gets a time zone only when you choose one; without one, it is stored in
UTC. One that another app gave a time zone keeps it when you change it in
Lucid.
To keep your time zone out, use a browser that reports UTC instead, such as
Firefox with
privacy.resistFingerprinting,
which reports UTC or Iceland's time (Atlantic/Reykjavik). Lucid then shows
every time in that zone, and a repeating event or task you create gets it, as
Thunderbird does with its time zone set to UTC, so a weekly meeting at 9:00
stays at 9:00 UTC all year.
An event or task can have time zones of its own, one for its start and one for its end or due date, such as a flight that leaves Berlin at 10:00 and lands in New York at 13:00. Its details then show its times there as well, and the editor shows and accepts each time in its own time zone. "Other time zone" adds them to an event or task in yours, and ✕ goes back to yours. You find a time zone by its city in English, its name or its offset. A new time zone keeps the time you entered, so 10:00 in Berlin becomes 10:00 in New York, and the editor says when that is in your time zone.
A repeating event or task stays in its time zone, also when you change its repeat. A weekly meeting on Tuesdays at 18:00 in New York shows on Wednesdays in Berlin, in the calendar and in its details, while the editor shows it at 18:00 on Tuesdays in New York, where you change its repeat. A new time zone applies to all events of a series or the following ones, at the time you entered: moved to Berlin at 18:00, the meeting is at 18:00 every Tuesday in Berlin.
You need nothing but a CalDAV account: no server, no TLS, no configuration.
- Download the archive for your system from the
releases page and extract it.
Its name ends in your system and processor, like
lucid_X.Y.Z_darwin_arm64.tar.gz:- Windows:
windows_amd64.zipfor most PCs,windows_arm64.zipfor ARM devices such as Snapdragon laptops. - macOS (called
darwin):darwin_arm64.tar.gzfor Apple silicon (M1 and later),darwin_amd64.tar.gzfor Intel Macs. - Linux:
linux_amd64.tar.gzfor most PCs,linux_arm64.tar.gzfor ARM machines such as a Raspberry Pi 4 or 5 with a 64-bit OS.
- Windows:
- Double-click
lucid(lucid.exeon Windows). - A window opens and your browser shows Lucid at http://127.0.0.1:3456/. Log in with your CalDAV server's address (a bare domain is enough), your username and your password.
Close the window to stop Lucid. Closing the browser tab does not stop it.
- macOS: Gatekeeper blocks apps that are not signed by an Apple
developer, and Lucid is not. Open System Settings, go to Privacy & Security
and click "Open Anyway" next to the message about
lucid. - Windows: SmartScreen warns about the same. Click "More info", then "Run anyway".
- Linux: run
./lucidin a terminal. Ctrl+C stops it. With the deb, rpm or Arch package (On a server), start "Lucid" from the app menu instead; it opens a terminal window, and closing it stops Lucid.
This mode is meant for your own computer. Lucid listens on 127.0.0.1 only, so
no other device can reach it, and it serves plain HTTP because the traffic never
leaves your machine. Do not use it on a computer shared with people you do not
trust. CalDAV servers on your own computer or network (a Radicale on your NAS,
for example) work without further setup. To use Lucid from other devices, run
it on a server (below).
You log in again after each start, because sessions live in memory only. If
port 3456 is taken, Lucid says so and exits (on Windows the window stays open
until you press Enter). To choose another port, set LUCID_LOCAL_PORT in a
terminal, for example LUCID_LOCAL_PORT=4000 ./lucid on Linux and macOS, or
$env:LUCID_LOCAL_PORT = "4000"; .\lucid.exe in Windows PowerShell.
Important
TLS is mandatory on a server. Lucid sets Secure session cookies and
handles your CalDAV credentials. Run it behind a TLS-terminating reverse
proxy (Caddy, Traefik, nginx, ...) or let it serve HTTPS itself via
LUCID_TLS_CERT / LUCID_TLS_KEY.
Setting LUCID_ADDR makes Lucid a server (the container image and
compose.yaml set it for you, the Nix modules with mode = "server"); without
it, Lucid runs in local mode.
The repository contains a hardened example compose.yaml
(read-only root filesystem, all capabilities dropped, non-root user,
no-new-privileges), bound to 127.0.0.1:8080 for your reverse proxy:
curl -fsSLO https://raw.githubusercontent.com/tbckr/lucid/stable/compose.yaml
docker compose up -dMinimal Caddy configuration:
calendar.example.com {
reverse_proxy 127.0.0.1:8080
}Then open Lucid and log in with your CalDAV server's address (a bare domain is enough), your username and your password.
Docker
docker run -d --name lucid \
-p 127.0.0.1:8080:8080 \
--read-only --cap-drop ALL --security-opt no-new-privileges:true \
ghcr.io/tbckr/lucid:latestImages are published for linux/amd64 and linux/arm64, based on
gcr.io/distroless/static and running as UID/GID 65532.
Binary
Download the archive for your platform from the releases page (Linux, macOS, Windows; amd64 and arm64), verify it (see Verifying releases) and run:
LUCID_ADDR=127.0.0.1:8080 ./lucid
./lucid --versionHealth endpoints: GET /healthz (liveness), GET /readyz (readiness).
lucid healthcheck probes the local instance and exits non-zero on failure
(used by the container HEALTHCHECK, since distroless has no shell or curl).
Debian, Ubuntu, Fedora, Arch (package)
Each release has Linux packages for amd64 and arm64: a .deb, an .rpm and
an Arch .pkg.tar.zst. They install Lucid with a hardened systemd service for
server mode, which they do not enable, and an app-menu entry for local mode.
They need systemd 247 or later: Debian 12, Ubuntu 22.04, RHEL 9 and its
clones, Fedora, Arch, or newer. There is no package repository; to update,
install the next release's package the same way.
Download the package from the
releases page, verify it against
checksums.txt (see Verifying releases) and
install it:
sudo apt install ./lucid_X.Y.Z_linux_amd64.deb # Debian, Ubuntu
sudo dnf install ./lucid_X.Y.Z_linux_amd64.rpm # Fedora, RHEL
sudo pacman -U ./lucid_X.Y.Z_linux_amd64.pkg.tar.zst # ArchThe packages carry no signature of their own; the signed checksums.txt
covers them, so verify the package before you install it. If dnf refuses the
rpm as unsigned (it checks local packages where localpkg_gpgcheck is set),
add --nogpgcheck.
Then configure and start the service:
sudoedit /etc/lucid/lucid.env # LUCID_* settings, see Configuration
sudo systemctl enable --now lucid
journalctl -u lucid # logs/etc/lucid/lucid.env runs server mode on 127.0.0.1:8080 behind a reverse
proxy on the same machine, like compose.yaml; updates keep your changes.
To let Lucid serve TLS itself, pass the certificate and key in a drop-in
(sudo systemctl edit lucid), since the service runs as a dynamic user that
cannot read them:
[Service]
LoadCredential=tls-cert:/etc/ssl/lucid/cert.pem
LoadCredential=tls-key:/etc/ssl/lucid/key.pem
Environment=LUCID_TLS_CERT=/run/credentials/lucid.service/tls-cert
Environment=LUCID_TLS_KEY=/run/credentials/lucid.service/tls-keyThen set LUCID_ADDR in /etc/lucid/lucid.env to the public address (for
example :8443) and remove LUCID_TRUST_PROXY_HEADERS. A port below 1024
needs one more drop-in; a reverse proxy remains the simpler choice:
[Service]
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
PrivateUsers=noThe repository is a Nix flake with the package, an overlay, a NixOS module
and a Home Manager module. Both modules have the same options under
services.lucid, one for each variable in Configuration,
and run Lucid in local mode unless you set mode = "server". Setting an
option of the other mode is an error when the configuration is evaluated. To
try Lucid without installing it: nix run github:tbckr/lucid/stable (local
mode).
github:tbckr/lucid/stable is the latest release: the stable branch moves to
each release once it is published, pre-releases excepted, so
nix flake update brings you the next one. To stay on one release, use its
tag (github:tbckr/lucid/v1.2.0); without a ref, github:tbckr/lucid is the
development state of main. A flake cannot see tags, so lucid --version of
a Nix build shows nix- and the commit instead of the version number
(nix-a10d937).
NixOS
services.lucid runs Lucid as a hardened systemd service with a dynamic user,
started at boot:
{
inputs.lucid.url = "github:tbckr/lucid/stable";
outputs =
{ nixpkgs, lucid, ... }:
{
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
lucid.nixosModules.default
{ services.lucid.enable = true; }
];
};
};
}On your own computer (local mode, the default), Lucid listens on
127.0.0.1:3456 (port changes it). Open http://127.0.0.1:3456/ or the
"Lucid" entry in your app menu. One instance serves every user of the
computer, each with their own CalDAV account.
On a server behind a reverse proxy, any proxy works; Lucid only needs to know that one is in front of it:
services.lucid = {
enable = true;
mode = "server"; # listens on 127.0.0.1:8080
trustProxyHeaders = true;
};
# nginx (with security.acme set up) ...
services.nginx.virtualHosts."calendar.example.com" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://127.0.0.1:8080";
# Sends X-Forwarded-For; without it all clients share one rate limit.
recommendedProxySettings = true;
};
};
# ... or Caddy
services.caddy.virtualHosts."calendar.example.com".extraConfig = ''
reverse_proxy 127.0.0.1:8080
'';On a server with its own TLS, the certificate and key can come from any source (ACME, agenix, sops-nix, plain files). systemd reads them and hands them to Lucid as credentials, so the service needs no access to the files, and they never enter the Nix store:
services.lucid = {
enable = true;
mode = "server";
address = "0.0.0.0";
port = 443;
openFirewall = true;
tls.certFile = "/var/lib/acme/calendar.example.com/fullchain.pem";
tls.keyFile = "/var/lib/acme/calendar.example.com/key.pem";
};
# If security.acme manages the certificate: start Lucid once the files
# exist, and restart it on renewal.
systemd.services.lucid = {
wants = [ "acme-calendar.example.com.service" ];
after = [ "acme-calendar.example.com.service" ];
};
security.acme.certs."calendar.example.com".reloadServices = [ "lucid.service" ];Lucid reads the certificate only when it starts, so a renewal restarts it, and
everyone logs in again (sessions live in memory). A port below 1024 gives the
service CAP_NET_BIND_SERVICE and turns off PrivateUsers; everything else
stays as restricted as with a higher port.
Home Manager (Linux and macOS)
For computers without NixOS or without root: lucid.homeModules.default runs
Lucid under your own user, as a systemd user service on Linux and a launchd
agent on macOS, started when you log in:
home-manager.lib.homeManagerConfiguration {
# pkgs = ...;
modules = [
lucid.homeModules.default
{ services.lucid.enable = true; }
];
}With Home Manager as a NixOS or nix-darwin module, add it with
home-manager.sharedModules = [ lucid.homeModules.default ];. The options are
those of the NixOS module, without openFirewall.
- In local mode, open http://127.0.0.1:3456/; on Linux there is also a
"Lucid" entry in the app menu. Two users of one computer need different
ports. - Logs:
journalctl --user -u lucidon Linux,~/Library/Logs/lucid.logon macOS. - A user service on Linux starts at login. To start it at boot, for example
for server mode, run
loginctl enable-linger <user>once. - A Linux user service cannot listen on a port below 1024; use a reverse proxy or a higher port.
- Your user must be able to read
tls.certFileandtls.keyFile: on Linux your own systemd user manager reads them, on macOS Lucid does.
Lucid is configured exclusively through environment variables; there are no secrets in the configuration (the session key is random per start, the TLS key is a file).
Lucid runs in local mode when LUCID_ADDR is unset and in server mode
when it is set. The Mode column says which mode a variable belongs to. A
variable set for the other mode (even to its default value), or one of the
variables of earlier versions that no longer exist, stops Lucid at startup with
a message that names the variable. For a variable of the other mode it says
which mode that is, for a removed one what replaces it or why it is gone.
All environment variables
| Variable | Mode | Default | Description |
|---|---|---|---|
LUCID_ADDR |
Server | (unset) | Listen address, e.g. :8080. Setting it selects server mode; unset runs local mode |
LUCID_LOCAL_PORT |
Local | 3456 |
Port on 127.0.0.1. The only port Lucid tries: if it is taken, Lucid exits |
LUCID_OPEN_BROWSER |
Local | true |
Open the browser at Lucid's address once it is listening. false for development, tests and headless machines |
LUCID_METRICS_ADDR |
Server | (unset) | Address of a separate listener that serves /metrics. Unset: no /metrics at all |
LUCID_TLS_CERT, LUCID_TLS_KEY |
Server | (unset) | Paths of the certificate and key files, both or neither. Serves HTTPS directly. Otherwise TLS must be terminated by a reverse proxy |
LUCID_TRUST_PROXY_HEADERS |
Server | false |
Use X-Forwarded-For for the client IP (only behind a trusted proxy) |
LUCID_PRIVATE_NETWORKS |
Both | local all, server none |
Internal addresses (loopback, private, link-local, ...) that CalDAV servers may have: all allows every one, none none, and a comma-separated list of CIDRs and addresses (e.g. 192.168.1.10/32) only those. all and none are case-insensitive and cannot be mixed with addresses |
LUCID_SESSION_TTL |
Both | 12h |
Absolute session lifetime |
LUCID_SESSION_IDLE_TIMEOUT |
Both | 2h |
Idle timeout, at most LUCID_SESSION_TTL |
LUCID_RATE_LIMIT_RPS |
Both | 20 |
Token bucket refill per client IP (API) |
LUCID_RATE_LIMIT_BURST |
Both | 60 |
Token bucket size |
LUCID_LOGIN_RATE_LIMIT_PER_MIN |
Both | 10 |
Login attempts per client IP per minute |
LUCID_CACHE_SIZE |
Both | 256 |
Cached entries (LRU), one per user, calendar and component (events or tasks) |
LUCID_CACHE_FRESHNESS |
Both | 10s |
Serve cache without CTag check for this long |
LUCID_UPSTREAM_TIMEOUT |
Both | 20s |
Timeout for requests to CalDAV servers |
LUCID_LOG_LEVEL |
Both | info |
debug, info, warn, error. Logs go to stdout: text in local mode, JSON in server mode; debug adds a line for each API request and each CalDAV call, without contents and with pseudonymised paths, see Reporting problems |
Durations use Go syntax (90s, 15m, 12h).
The REST API is documented in docs/API.md.
- Terminate TLS at the proxy and forward plain HTTP to Lucid on a private
interface (e.g.
127.0.0.1:8080). Never expose the plain-HTTP port publicly. - Set
LUCID_TRUST_PROXY_HEADERS=trueonly if Lucid is reachable exclusively through the proxy; otherwise clients can spoofX-Forwarded-Forand evade rate limiting. - If your CalDAV server runs on your LAN, allow it narrowly with a CIDR in
LUCID_PRIVATE_NETWORKS(e.g.192.168.1.10/32) rather thanall. /metricsis served only on the separate addressLUCID_METRICS_ADDR, never on the main listener. Keep that address internal; without it there are no metrics.
Calendar servers differ in small ways, and a problem with yours is easiest to fix if the maintainers can see what your server sent. Lucid helps you share that without sharing your calendar.
Choose Report a problem in the account menu. The same dialog opens from an
error message that shows a reference such as Reference: 3f9a1c2b (also on the
login screen), from the login screen after Lucid signed you out without you
asking, from the placeholder of an item Lucid could not read, and from
the life-buoy button Add to problem report in the details of any event or
task, because the likeliest bugs, a wrong time or a missing repeat, leave no
error behind.
The dialog shows the file exactly as it is downloaded. By default it holds:
- the Lucid version, its mode and the operating system,
- the product and host of your CalDAV server, the endpoint Lucid found, and the paths of your principal and calendar home, with placeholders for the names in them, as in the CalDAV calls,
- what each calendar supports and how many items it holds, never its name,
- your recent CalDAV calls (up to 100, 30 before you sign in): method, path, status, duration and a few response headers, without any request or response body,
- what Lucid could not use in your server's answers, as structure only: property names and the format of values, with titles, notes, places, names, addresses and IDs replaced (time zone and repeat rules stay, as bugs hide in them),
- the field names that failed in items Lucid could not read,
- your browser's time zone, language and user agent, the size of its window, and the settings that change what Lucid shows: language, time format, week start, whether done tasks are hidden in the calendar, and how many of your calendars you hid (not which).
In every path in the file, your login name is replaced by {user} and every
other name, of a calendar, an item or an account ID, by a placeholder; only
names that make up how CalDAV servers are laid out, such as remote.php or
calendars, stay. No title, description, place or attendee is in the file.
The host of your CalDAV server is the one name that stays readable: mask it
before you post the file if you would rather not publish it. For an event or
task you add, the file
holds its structure. Its content is added only if you choose Show
content, read it, and tick the box Add this content to the report, which
starts unticked, for that one item.
To add several events or tasks, choose Add another item (or close the dialog): the report then waits at the bottom of the screen, with the items in it, until you download or discard it. The life buoy of the next item adds it and opens the report again. While it waits, Lucid keeps what you showed and ticked, in your browser only; downloading, discarding or signing out forgets it, and so does closing a report without items.
Lucid sends nothing itself: the report is a file you download. Open issue
form opens the issue form
in a new tab, with Lucid's version and mode and your server product filled
in, and you attach the file there (if GitHub refuses the .json file, rename
it to .txt or paste its content). Issues on GitHub are public, so read the
file first. Report security problems privately, see SECURITY.md.
If Lucid shows Something broke and reloading doesn't help, open Error details below the button: copy them with Copy details and paste them into the bug form. They hold Lucid's version, your browser's time zone, language and user agent, and the error; read them first, as an error can name an item of your calendar. Lucid keeps the calls and problems in memory only, for as long as your session lasts.
Lucid never logs calendar contents or passwords, at any level and in either
mode, and no setting changes that. With LUCID_LOG_LEVEL=debug it adds a
caldav exchange line for every request to a CalDAV server (method, host,
path, status, duration, size, content type, the Server and DAV headers and,
for a transport failure, a class such as dns, tls or timeout) and an
http request line for every API request. A caldav problem line, written at
warn and so at the default level too, comes with the structure of the
document Lucid could not read.
- Find a user's request. Every response has an
X-Request-Id, and every line logged while serving the request has it asrequest_id. The reference a user sees in an error message is its first 8 characters, so searching the log for it (for examplegrep 3f9a1c2b) finds the request. One exception: acaldav problemline for an item Lucid cannot read or show is written only once per Lucid process, with the request ID of the first read that met it, so a reference may not lead to it. Thepathof such a line is the one in the report'sproblems, until Lucid restarts. - Paths are pseudonymised. The user name is
{user}, the calendar home and principal are{home}and{principal}, and the names below the home become placeholders such ascal-1a2b3c4d/,obj-5e6f7a8b.icsandseg-<h>for any other name. They are derived with a key that exists only in RAM: the same name has the same placeholder until Lucid restarts, and none can be turned back into a name. API IDs in URLs are encrypted with a key that exists only in RAM, so your reverse proxy's log shows no names either, and the IDs change on restart. One thing in a URL is no pseudonym: a change to one occurrence of a repeating event or task has the occurrence's start time in its URL (.../occurrences/<time>,.../following/<time>), so your reverse proxy's log and thehttp requestlines show that time, though not the event's or task's name or content. The host of the CalDAV server stays, so you can tell servers apart. So do structural names outside the home and principal, such asremote.php,dav,principalsandcalendars(a name somebody chose that is one of these words stays as well), so that a discovery path remains readable. - Share the CalDAV lines, not all of them. The
caldav exchangeandcaldav problemlines can go into a public issue.security eventlines record a login with the user name and server URL entered, for abuse detection, and carry a client IP address;http requestlines carry client IP addresses too. Keep both to yourself.
- Proxy architecture. The browser only talks to Lucid; Lucid talks to the
CalDAV server. This avoids CORS problems and keeps CalDAV credentials out of
the browser: after login the frontend only holds an opaque session cookie
(
HttpOnly,SameSite=Strict, andSecurein server mode; local mode serves plain HTTP on the loopback interface only and sets noSecureflag). - Encrypted in-memory sessions. Credentials are stored server-side, encrypted with AES-256-GCM under a key that is random per start, and only in RAM. Nothing is written to disk, and no key appears in the environment or the configuration.
- Local mode. It listens on
127.0.0.1only and accepts requests only when theHostheader islocalhostor127.0.0.1(any port), which protects against DNS rebinding. It is meant for your own computer: other users of that computer can reach the same address. - SSRF protection. Because users choose the server URL, every outgoing
request (including auto-discovery and redirects) goes through a safe
transport that refuses loopback, private, link-local and other internal
address ranges unless
LUCID_PRIVATE_NETWORKSallows them. Local mode allows them all by default, because you can reach your own network anyway; server mode allows none. - Web hardening. CSRF synchronizer tokens on all state-changing requests,
token-bucket rate limiting (stricter for login), strict security headers
(CSP, HSTS,
X-Frame-Options,X-Content-Type-Options), XML entity protection, input validation, open-redirect protection, no source maps in production builds. - Supply chain. Signed releases and images, SBOMs and daily vulnerability scans; see Supply chain.
To report a vulnerability, see SECURITY.md.
Bug reports, feature ideas and pull requests are welcome. Report bugs through
the issue forms, and bring
ideas and questions to Discussions.
CONTRIBUTING.md explains the development setup
(just dev starts Lucid with a demo CalDAV server) and the project's
conventions.
Lucid is free software licensed under the GNU General Public License v3.0.






