There is an abundance of self-hosted CalDav servers out there, yet basically nothing to actually display the data with!
Luna aims to be a self-hosted CalDav calendar client web application. That is, Luna will let you connect to CalDav servers, iCal links and more. It will manage all the connections in the background and serve you a view with all your calendars and their events, as you have come to expect from a commercial calendar application.
Ultimately, Luna should become a fully offline-capable Progressive Web Application (PWA), so you can still manage your calendars without an internet connection. Upon reconnecting to the internet, all your events will synchronize with the backend.
This approach ensures out-of-the-box platform cross-compatibility: You will be able to use Luna inside your favourite web browser, as well as install it as a PWA.
Luna is an ambitious and large project. As such, development takes a long time.
At this point in time, Luna is nearing a usable 1.0.0 state. A lot of small and less small finishing touches still need to be done (in particular, mobile screen size support). If you'd like to see a preview of the project for yourself, refer to the deployment section.
Due to my busy schedule and being the sole developer, I am unable to provide a release date for 1.0.0 for now. Feel free to get in contact through GitHub discussions if you are interested in contributing.
You may also follow the progress in the development roadmap.
Since Luna is not ready to be used yet, this only serves as instructions on how to get Luna up and running for development purposes!
Keep in mind that Luna is provided with absolutely no warranty or liability from the authors.
Currently, no first-party docker images are available. Instead, you can generate and run the images simply by typing make in the root directory of this repository.
Make sure you have make and docker installed.
Until docker images are generated officially, you can also use community-compiled images for the frontend and the backend. These images are provided with no warranty or liability from the main author.
For baremetal deployment, you must ensure your system has:
- make
- bun (v1.2.5 or higher)
- go (go1.23 or higher)
- a running postgres (version 16) database
For the backend, create an .env file in the backend directory inside the repository and fill it out accordingly to .env.example. To start the backend, run make inside the backend directory.
Proceed in the same way for the frontend inside the frontend directory.
- All bodies are to be passed as
multipart/form-data. - All endpoints except unauthenticated ones require an access token received from the Login endpoint. It is to be passed in the request header as a bearer token or as the cookie token.
- Parameters passed via the URL are indicated with angular brackets, e.g.
<ID> - In case of users,
selfcan be used in place of<ID>to indicate the calling user.
An early draft was to address calendars in explicit relation to their sources and events to their calendars. For example, editing an event would have worked through /api/sources/<SourceID>/calendars/<CalendarID>/events/<EventID>. This was later scrapped in favour of the much simpler endpoints /api/calendars and /api/events.
The reasoning for this change is that Luna is supposed to be a calendar aggregator as one of its main principles. After adding one's sources, the user should no longer need to care about them when viewing or manipulating calendars and events. While this simplification of the API makes the implementation of the backend slightly more challenging, it is worth the effort in my opinion.
Luna uses UUIDs for all its IDs. This has a few reasons:
- Avoiding conflicts in distributed scenarios (future-proofing)
- Better security thanks to unpredictable IDs; in particular, a potential attacker can neither guess IDs of any resources, nor can they deduct information from IDs, like amount of registered users (since IDs are not consecutive).
- While UUIDv4 is used as a base for some IDs to ensure uniquity and unpredictability, other IDs are built on top of these pseudo-random identifiers using the deterministic UUIDv5. This determinism built on top of random "base" IDs provides design-level collision resistance while maintaining deterministic ways to derive the IDs.
Luna uses its own (UU)IDs for every resource accessed through it. Therefore, the ID, over which you access a calendar or an event over Luna is different from the underlying IDs used by the upstream source. This has a few reasons:
- Different sources might use different ID types. Luna instead uses the same ID scheme for everything.
- Better security thanks to hiding the nature of the upstream sources from potential eavesdroppers.
- If an event with the same ID is present in two different calendars (this can have legitimate operational reasons), Luna will still be able to distinguish between them thanks to different internal IDs.
- Path:
/api/login - Method:
POST - Body:
username,password,remember - Purpose: Returns an authorization token
- Path:
/api/register - Method:
POST - Body:
username,password,email,remember - Purpose: Creates a new user
- Path:
/api/register/enabled - Method:
GET - Body: Empty
- Purpose: Check if registration is open for everyone.
- Path:
/api/version - Method:
GET - Body: Empty
- Purpose: Returns the current backend version. This will be used by the frontend to verify compatibility based on the major version.
- Path:
/api/health - Method:
GET - Body: Empty
- Purpose: Determines whether the frontend, the backend, and the database are all functioning correctly.
- Path:
/api/users/<ID> - Method:
GET - Body: Empty
- Purpose: Returns the user's saved data, like username and email address.
- Path:
/api/users - Method:
GET - Search Parameters:
all(falseby default,trueto also include disabled or non-searchable accounts) - Purpose: Returns the user's saved data, like username and email address.
- Path:
/api/users/<ID> - Method:
PATCH - Body: Depending on which the user wants to change:
username,new_password,email,pfp_type,pfp_url,pfp_file,searchable. The old passwordpasswordis required if any ofusername,new_password, oremailare specified. - Purpose: Changes the user's data.
- Path:
/api/users/<ID> - Method:
DELETE - Body:
password - Purpose: Deletes the user account.
- Path:
/api/users/<ID>/disable - Method:
POST - Body: Empty
- Purpose: Disables user account (delete sessions and prevents login).
- Path:
/api/users/<ID>/enable - Method:
POST - Body: Empty
- Purpose: Enables user account (allow login again).
- Path:
/api/sources - Method:
GET - Body: Empty
- Purpose: Returns a list of the user's calendar sources.
- Path:
/api/sources/<ID> - Method:
GET - Body: Empty
- Purpose: Returns details for a user's specific source, including authentication data.
- Path:
/api/sources - Method:
PUT - Body:
name,type,auth_type - Purpose: Puts a new calendar source in the database. The authentication information is encrypted by PostegreSQL.
Depending on the type field, additional information may need to be passed:
caldav:urlical:location(one ofremote,databaseorlocal)url(if chosenremote)file(if chosendatabase)path(if chosenlocal)
Depending on the auth_type field, additional information may need to be passed:
none: No additional informationbasic:username,passwordbearer:tokenoauth: Not yet implemented
- Path:
/api/sources/<ID> - Method:
PATCH - Body:
name,type,auth_type, depending on which values should be updated. Iftypeandauth_typeare set, additional information must be provided, as described in the Put Source endpoint - Purpose: Edit an existing source
- Path:
/api/sources/<ID> - Method:
DELETE - Body: Empty
- Purpose: Deletes a source from the database.
- Path:
/api/sources/<ID>/calendars - Method:
GET - Body: Empty
- Purpose: Fetches calendars from the specified source.
- Path:
/api/calendars/<ID> - Method:
GET - Body: Empty
- Purpose: Fetches a specific calendar from its appropriate source.
- Path:
/api/sources/<ID>/calendars - Method:
PUT - Body:
name,color - Purpose: Add a new calendar to the specified source in the upstream, as well as the local database.
- Path:
/api/calendars/<ID> - Method:
PATCH - Body:
name,color, depending on which values should be updated. - Purpose: Updates specific fields of a calendar in the local database and the upstream source.
- Note: This endpoint strives to not erase any values set by other applications that are not supported by Luna.
- Path:
/api/calendars/<ID> - Method:
DELETE - Body: Empty
- Purpose: Deletes the source from the local database and the upstream source.
- Path:
/api/calendars/<ID>/events - Method:
GET - Search Parameters:
start,end(both in RFC-3339 format and at most one year apart) - Purpose: Fetches events from the specified calendar.
- Path:
/api/events/<ID> - Method:
GET - Body: Empty
- Purpose: Fetches a specific event from its appropriate calendar.
- Path:
/api/calendars/<ID>/events - Method:
PUT - Body:
name,desc,color,date_start,date_end,date_duration - Purpose: Add a new event to the specified calendar in the upstream, as well as the local database.
The description field is optional. Either the end date or the event duration is to be specified, not both and not neither.
- Path:
/api/events/<ID> - Method:
PATCH - Body:
name,desc,color,date_start,date_end,date_duration, depending on which values should be updated. - Purpose: Updates specific fields of an event in the local database and the upstream source.
- Note: If
descshould not change, it must be set to its previous values, since leaving it empty implies deleting the description. This endpoint strives to not erase any values set by other applications that are not supported by Luna.
The description field is optional. Either the end date or the event duration is to be specified, not both and not neither.
- Path:
/api/events/<ID> - Method:
DELETE - Body: Empty
- Purpose: Deletes the event from the local database and the upstream source.
- Path:
/api/files/<ID> - Method:
GET - Body: Empty
- Purpose: Returns a file from the database
- Note: There are currently no mechanisms in place determining which users may download which files. Any authenticated user with knowledge of the file ID can download this file. UUIDs do not provide enough security guarantees in this scenario. This should be revisited in the future.
- Path:
/api/files/<ID> - Method:
HEAD - Body: Empty
- Purpose: Returns the name and size of a file in the database
- Path:
/api/settings - Method:
GET - Body: Empty
- Purpose: Returns all key-value pairs from the global settings
- Path:
/api/settings/<KEY> - Method:
GET - Body: Empty
- Purpose: Returns a specific key-value pair from the global settings
- Path:
/api/settings - Method:
PATCH - Body: Key-value pairs to change with value as a serialized JSON object
- Purpose: Sets specific key-value pairs in the global settings
- Note: This endpoint is only accessibly by an administrator
- Path:
/api/settings - Method:
DELETE - Body: Empty
- Purpose: Reverts all global settings to their default values
- Path:
/api/settings/<KEY> - Method:
DELETE - Body: Empty
- Purpose: Reverts a global setting to its default value
- Path:
/api/users/<ID>/settings - Method:
GET - Body: Empty
- Purpose: Returns all key-value pairs from the requesting user's settings
- Path:
/api/users/<ID>/settings/<KEY> - Method:
GET - Body: Empty
- Purpose: Returns a specific key-value pair from the requesting user's settings
- Path:
/api/users/<ID>/settings - Method:
PATCH - Body: Key-value pairs to change with value as a serialized JSON object
- Purpose: Sets specific key-value pairs in the global settings
- Path:
/api/users/<ID>/settings - Method:
DELETE - Body: Empty
- Purpose: Reverts all of the requesting user's settings to their default values
- Path:
/api/users/<ID>/settings/<KEY> - Method:
DELETE - Body: Empty
- Purpose: Reverts the requesting user's setting to its default value
- Path:
/api/sessions - Method:
GET - Body: Empty
- Purpose: Returns all currently authorized sessions of the calling user
- Path:
/api/sessions/<ID>/permissions - Method:
GET - Body: Empty
- Purpose: Returns the administrator status and all permissions associated with the used session token
- Path:
/api/sessions/valid - Method:
GET - Body: Empty
- Purpose: Returns whether the current user session is valid
- Path:
/api/sessions - Method:
PUT - Body:
name,password - Purpose Creates a return new API token
- Path:
/api/sessions/<ID> - Method:
PUT - Body:
name,password - Purpose Modifies an API token
- Path:
/api/sessions/<ID> - Method:
DELETE - Body: Empty
- Purpose: Unauthorizes a specific session
- Note: The
<ID>parameter can be set tocurrentto refer to the currently used session.
- Path:
/api/sessions?type=<TYPE> - Method:
DELETE - Body: Empty
- Purpose: Unauthorizes all sessions of the calling user
- Note: The
<TYPE>parametert should be set touser,api, orall, indicating which types of sessions should be revoked.
- Path:
/api/url - Method:
POST - Body:
url,auth_type - Purpose: Tries to determine if the supplied URL links to an iCal file or a CalDAV server. In case of a CalDAV server, it also returns the principal's base URL.
Depending on the auth_type field, additional information may need to be passed:
none: No additional informationbasic:username,passwordbearer:tokenoauth: Not yet implemented
- Path:
/api/invites - Method:
GET - Body: Empty
- Purpose: Returns all active registration invites
- Path:
/api/invites/<ID>/qr - Method:
GET - Body: Empty
- Purpose: Returns a QR code image of the specified invitation link
- Path:
/api/invites - Method:
PUT - Body:
durationin seconds - Purpose: Creates a new registration invite
- Path:
/api/invites/<ID> - Method:
DELETE - Body: Empty
- Purpose: Retracts a registration invite
- Path:
/api/invites - Method:
DELETE - Body: Empty
- Purpose: Retracts all registration invites
Aside from using the backend API, the frontend also provides a limited amount of endpoints for its own purposes. They are to be used in the same way as the backend endpoints regarding authentication and body format.
All the following endpoints require the caller to be an authenticated user.
Additionally, both the PUT and the DELETE method requires the user to be an administrator.
- Path:
/installed/fonts - Method:
GET - Body: Empty
- Purpose: Returns the names and paths of installed fonts.
- Path:
/installed/fonts - Method:
PUT - Body:
filecontaining the font with a.ttfextension - Purpose: Installs a new font in the frontend.
- Path:
/installed/fonts/<FILE> - Method:
DELETE - Body: Empty
- Purpose: Deletes an installed font from the frontend.
- Path:
/installed/themes - Method:
GET - Body: Empty
- Purpose: Returns the names and paths of installed themes.
- Path:
/installed/themes - Method:
PUT - Body:
filecontaining the theme with a.ccsextension - Purpose: Installs a new theme in the frontend.
- Path:
/installed/fonts/<FILE> - Method:
DELETE - Body: Empty
- Purpose: Deletes an installed theme from the frontend.