A minimal server that converts external APIs into webcal/ICS calendar subscriptions.
- Runtime: Bun - Fast TypeScript runtime with built-in HTTP server
- Calendar: ts-ics - ICS/WebCal format generation
- Testing: Bun test - Built-in test runner
- Linting/Formatting: Biome - Fast formatter and linter
- Git Hooks: Husky - Git hooks made easy
- Disk caching with configurable CRON-based refresh
- Integration-specific query parameters for customization
- Webcal subscriptions with copyable HTTP(S) calendar URLs
- Emoji prefixes for event types
- Completed events marked with ✅
Feature specifications live in the docs/ folder.
| Document | Description |
|---|---|
| Server & Routing | HTTP server and request routing |
| Caching | Disk-based response caching |
| Scheduler | CRON-based cache refresh |
| Web UI | Subscription form interactions |
Each integration lives in its own folder under src/integrations/ with its own README.
| Integration | Endpoint | Documentation |
|---|---|---|
| IBU Biathlon World Cup | /api/ibu/wc.ics |
src/integrations/ibu/README.md |
| Tomorrowland | /api/tomorrowland/lineup.ics |
src/integrations/tomorrowland/README.md |
See Adding a New Integration for how to create new integrations.
livecal/
├── docs/ # Feature documentation
│ ├── cache.md # Disk caching
│ ├── scheduler.md # CRON scheduler
│ └── server.md # HTTP server & routing
│
├── src/
│ ├── index.ts # Application entry point (Bun.serve)
│ │
│ ├── server/ # Server core functionality
│ │ ├── router.ts # Route definitions and request handling
│ │ ├── cache.ts # Disk caching logic
│ │ └── scheduler.ts # CRON scheduler for cache updates
│ │
│ ├── integrations/ # All calendar integrations
│ │ ├── index.ts # Integration registry
│ │ │
│ │ ├── framework/ # Declarative integration framework
│ │ │ ├── types.ts # CalendarIntegration type
│ │ │ ├── params.ts # ParamSchema types & validation
│ │ │ ├── handlers.ts # Route handlers (calendar, options)
│ │ │ ├── register.ts # createIntegration() factory
│ │ │ └── index.ts # Public exports
│ │ │
│ │ ├── ibu/ # IBU Biathlon integration
│ │ │ ├── README.md # Integration documentation
│ │ │ ├── types.ts # API response types
│ │ │ ├── definition.ts # Declarative integration definition
│ │ │ ├── api.ts # External API client
│ │ │ ├── events.ts # Transform data to ICS events
│ │ │ ├── calendar.ts # Legacy calendar builder (for tests)
│ │ │ └── duration.ts # Event duration estimation
│ │ │
│ │ └── tomorrowland/ # Tomorrowland integration
│ │ ├── README.md # Integration documentation
│ │ ├── types.ts # API response types
│ │ ├── definition.ts # Declarative integration definition
│ │ ├── api.ts # External API client
│ │ ├── events.ts # Transform data to ICS events
│ │ └── calendar.ts # Legacy calendar builder (for tests)
│ │
│ └── static/ # Static files (homepage, etc.)
│
└── tests/
├── ibu-calendar.test.ts # IBU calendar output tests
├── tomorrowland-calendar.test.ts # Tomorrowland calendar output tests
└── tomorrowland-options.test.ts # Tomorrowland options endpoint tests
Integrations are defined declaratively - you specify what your integration needs, and the framework handles routing, validation, and ICS generation.
- Create a new folder under
src/integrations/:
src/integrations/
└── myservice/
├── README.md # Integration documentation
├── types.ts # API response types
├── definition.ts # Declarative integration definition
├── api.ts # External API client
└── events.ts # Transform data to ICS events
- Define the integration (
definition.ts):
import type { IcsEvent } from 'ts-ics'
import { createIntegration } from '../framework/index.ts'
import { fetchItems } from './api.ts'
import type { MyItem, MyParams } from './types.ts'
export default createIntegration<MyItem[], MyParams>({
// Identity
id: 'myservice',
name: 'My Service',
description: 'Choose filters to customize this calendar.',
// Calendar metadata
calendar: {
prodId: '-//Livecal//My Service//EN',
name: 'My Service Calendar', // Can also be a function: (params) => `Calendar ${params.filter}`
},
// Endpoint: creates /api/myservice/calendar.ics
endpoint: 'calendar.ics',
// Parameter schema - defines validation, parsing, and UI metadata
params: {
filter: {
type: 'text',
label: 'Filter',
placeholder: 'Enter filter text',
},
category: {
type: 'select',
label: 'Category',
required: true,
options: [
{ value: 'all', label: 'All' },
{ value: 'active', label: 'Active Only' },
],
},
showCompleted: {
type: 'checkbox',
label: 'Show Completed',
default: false,
},
},
// Fetch data from external API
fetchData: async params => {
return fetchItems(params.category, params.filter)
},
// Transform data to ICS events
toEvents: (items, params) => {
let filtered = items
if (!params.showCompleted) {
filtered = filtered.filter(item => !item.completed)
}
return filtered.map(item => ({
uid: item.id,
stamp: { date: new Date() },
start: { date: new Date(item.date) },
summary: item.title,
description: item.description,
}))
},
})- Register in
src/integrations/index.ts:
import myServiceIntegration from './myservice/definition.ts'
export const integrations = [
// ... existing integrations
myServiceIntegration,
]-
Add tests for the
toEventsfunction intests/myservice-calendar.test.ts -
Update this README with the new integration endpoint
| Type | Description | Options |
|---|---|---|
text |
Text input | required, default, placeholder, validate |
select |
Single-select dropdown | required, default, options, validate |
checkbox |
Boolean toggle | default |
dynamic-select |
Async multi-select | multiple, dependsOn, fetchOptions |
- No boilerplate: Routes, validation, and ICS generation are handled automatically
- Type-safe: Full TypeScript support with inferred parameter types
- Self-documenting: Parameter schema generates UI metadata for the frontend
- Testable: Pure
toEventsfunction makes testing straightforward
Create a .env file based on .env.example:
# Server
PORT=6699
HOST=0.0.0.0
# Cache
CACHE_DIR=./cache
CACHE_CRON="0 * * * *" # Every hour (cron format)- Bun >= 1.0
# Clone the repository
git clone <repository-url>
cd livecal
# Install dependencies
bun install
# Setup git hooks
bun run prepare
# Copy environment variables
cp .env.example .env| Script | Description |
|---|---|
bun run dev |
Start development server with hot reload |
bun run start |
Start production server |
bun test |
Run tests |
bun test --watch |
Run tests in watch mode |
bun run lint |
Run Biome linter |
bun run format |
Format code with Biome |
Tests use Bun's test runner and focus on verifying the final ICS calendar output for each integration.
# Run all tests
bun test
# Run tests in watch mode
bun test --watch
# Run specific test file
bun test tests/ibu-calendar.test.tsTest files are named {integration}-calendar.test.ts and test the buildCalendar() function output.
# Build production image
docker build -t livecal .
# Run container
docker run -p 6699:6699 livecal# Build stage
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
# Production stage
FROM oven/bun:1-slim
WORKDIR /app
# Create non-root user
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 bunjs
USER bunjs
COPY --from=builder --chown=bunjs:nodejs /app/dist ./dist
COPY --from=builder --chown=bunjs:nodejs /app/package.json ./
# Create cache directory
RUN mkdir -p /app/cache
ENV NODE_ENV=production
ENV PORT=6699
EXPOSE 6699
CMD ["bun", "run", "dist/index.js"]# docker-compose.yml
services:
livecal:
build: .
container_name: livecal
restart: unless-stopped
ports:
- '6699:6699'
environment:
- PORT=6699
- HOST=0.0.0.0
- CACHE_DIR=/app/cache
- CACHE_CRON=0 * * * *
- LOG_LEVEL=info
volumes:
- livecal-cache:/app/cache
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:6699/health']
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
volumes:
livecal-cache: