Keeper is a simple & open-source calendar syncing tool. It allows you to pull events from remotely hosted iCal or ICS links, and push them to your main calendar so the time slots can align across all your calendars.
- Aggregating calendar events from remote sources
- Event content agnostic syncing engine
- Push aggregate events to one or more calendars
- Open source under GPL-3.0
- Easy to self-host
- Easy-to-purge remote events
If you encounter a bug or have an idea for a feature, you may open an issue on GitHub and it will be triaged and addressed as soon as possible.
High-value and high-quality contributions are appreciated. Before working on large features you intend to see merged, please open an issue first to discuss beforehand.
Because I needed it. Ever since starting Sedna—the AI governance platform—I've had to work across three calendars. One for my business, one for work, and one for personal.
Meetings have landed on top of one-another a frustratingly high number of times.
I've probably tried it. It was probably too finicky, ended up making me waste hours of my time having to delete stale events it didn't seem to want to track anymore, or just didn't sync reliably.
- If we have a local event but no corresponding "source → destination" mapping for an event, we push the event to the destination calendar.
- If we have a mapping for an event, but the source ID is not present on the source any longer, we delete the event from the destination.
- Any events with markers of having been created by Keeper, but with no corresponding local tracking, we remove it. This is only done for backwards compatibility.
Events are flagged as having been created by Keeper either using a @keeper.sh suffix on the remote UID, or in the case of a platform like Outlook that doesn't support custom UIDs, we just put it in a "keeper.sh" category.
- Keeper tracks timeslots, not event details, summaries, descriptions, etc., for now. If you need that I would recommend OneCal.
- Keeper only sources from remote and publicly available iCal/ICS URLs at the moment, so that means that if your security policy does not permit these, another solution may suit you better.
I've made Keeper easy to self-host, but whether you simply want to support the project or don't want to deal with the hassle or overhead of configuring and running your own infrastructure cloud hosting is always an option.
Head to keeper.sh to get started with the cloud-hosted version. Use code README for 25% off.
| Free | Pro (Cloud-Hosted) | Pro (Self-Hosted) | |
|---|---|---|---|
| Monthly Price | $0 USD | $5 USD | $0 |
| Annual Price | $0 USD | $42 USD (-30%) | $0 |
| Refresh Interval | 30 minutes | 1 minute | 1 minute |
| Source Limit | 2 | ∞ | ∞ |
| Destination Limit | 1 | ∞ | ∞ |
By hosting Keeper yourself, you get all premium features for free, can guarantee data governance and autonomy, and it's fun.
If you'll be self-hosting, please consider supporting me and development of the project by sponsoring me on GitHub.
- Docker and Docker Compose
- (Optional) Google OAuth credentials for Google Calendar integration
- (Optional) Microsoft OAuth credentials for Outlook integration
- (Optional) Resend API key for email notifications
Running the following command will populate a .env file for you with required secrets for the Docker Compose configuration.
cat > .env << EOF
DOMAIN=http://localhost:3000
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
ENCRYPTION_KEY=$(openssl rand -base64 32)
EOFThe DOMAIN value should include the scheme and port (for example https://keeper.example.com) so the app can use it directly without accidentally producing malformed URLs like http://https://keeper.example.com:3000.
If you'd like to configure optional providers, you can refer to the Google OAuth documentation on configuring your Google client ID and secret, and Outlook's documentation can be found on the Microsoft documentation website where you can learn to configure Entra ID in the Azure portal to get your Microsoft client ID and secret.
Once you've acquired them, simply add them to the environment variables to make them available as destinations.
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=Caution
The following is an example of a minimum-viable compose.yaml which will define all the services to run Keeper. You can place it adjacent to the .env file we just created, but you should take a moment to securely configure the credentials for Redis, PostgreSQL, etc., away from the default values.
services:
postgres:
image: postgres:17
environment:
POSTGRES_USER: keeper
POSTGRES_PASSWORD: keeper
POSTGRES_DB: keeper
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U keeper -d keeper"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
api:
image: ghcr.io/ridafkih/keeper-api:latest
environment:
API_PORT: 3001
DATABASE_URL: postgres://keeper:keeper@postgres:5432/keeper
REDIS_URL: redis://redis:6379
BETTER_AUTH_URL: ${DOMAIN:-http://localhost:3000}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
COMMERCIAL_MODE: false
PASSKEY_RP_ID: ${DOMAIN:-localhost}
PASSKEY_RP_NAME: Keeper
PASSKEY_ORIGIN: ${DOMAIN:-http://localhost:3000}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}
ports:
- "3001:3001"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
cron:
image: ghcr.io/ridafkih/keeper-cron:latest
environment:
DATABASE_URL: postgres://keeper:keeper@postgres:5432/keeper
REDIS_URL: redis://redis:6379
BETTER_AUTH_URL: ${DOMAIN:-http://localhost:3000}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
MICROSOFT_CLIENT_ID: ${MICROSOFT_CLIENT_ID:-}
MICROSOFT_CLIENT_SECRET: ${MICROSOFT_CLIENT_SECRET:-}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
web:
image: ghcr.io/ridafkih/keeper-web:latest
environment:
API_URL: http://api:3001
ports:
- "3000:3000"
depends_on:
api:
condition: service_started
volumes:
postgres-data:
redis-data:Once this is done, Keeper will be available at http://localhost:3000 or the custom domain you've set. You can pair this configuration with a reverse-proxy. I personally use, prefer and recommend Caddy as it has a great configuration system and automatically manages certificates for you.
docker compose up -d- @keeper.sh/cli (Coming Soon)
- @keeper.sh/mobile (Coming Soon)
- @keeper.sh/ssh (Coming Soon)
- @keeper.sh/web
- @keeper.sh/auth
- @keeper.sh/auth-plugin-username-only
- @keeper.sh/broadcast
- @keeper.sh/broadcast-client
- @keeper.sh/calendar
- @keeper.sh/data-schemas
- @keeper.sh/database
- @keeper.sh/destination-providers
- @keeper.sh/destination-metadata
- @keeper.sh/encryption
- @keeper.sh/env
- @keeper.sh/eslint-config (Coming Soon)
- @keeper.sh/integration-caldav
- @keeper.sh/integration-fastmail
- @keeper.sh/integration-google-calendar
- @keeper.sh/integration-icloud
- @keeper.sh/integration-outlook
- @keeper.sh/integrations
- @keeper.sh/log
- @keeper.sh/oauth-google
- @keeper.sh/oauth-microsoft
- @keeper.sh/premium
- @keeper.sh/pull-calendar
- @keeper.sh/redis
- @keeper.sh/sync-calendar
- @keeper.sh/sync-events
- @keeper.sh/typescript-config