@svutil/cally
v0.0.5
Published
Calendar integrations with automatic token management
Readme
cally
Calendar sync that just works.
OAuth tokens, automatic refresh, delta sync, webhooks—handled. You bring the storage, cally handles the rest.
import { Calendar } from "@svutil/cally";
const cal = Calendar.microsoft({
clientId: process.env.MS_CLIENT_ID,
clientSecret: process.env.MS_CLIENT_SECRET,
adapter: tokenAdapter,
});
await cal.auth.setTokens(userId, tokens);
const events = await cal.events.list(userId);The Problem
Calendar integrations are deceptively annoying:
- Token refresh - Access tokens expire. Refresh tokens die. You need to handle both invisibly.
- Sync state - Microsoft's delta API is efficient but stateful. You need to track delta tokens per-user, per-calendar.
- Webhooks - Subscriptions expire after 3 days. Miss a renewal and you're polling blind.
- Error handling - Rate limits, permission changes, token revocation—each needs different handling.
Most teams end up building this infrastructure from scratch. It's not hard, but it's tedious and easy to get wrong.
What cally does
cally is a thin wrapper around Microsoft Graph (Google coming) that handles the boring parts:
- Invisible token refresh - Every API call checks expiry, refreshes if needed
- Delta sync - Full sync creates a checkpoint, incremental sync uses it
- Webhook lifecycle - Create, renew, parse notifications, handle edge cases
- Typed errors -
REAUTH_REQUIREDmeans prompt the user,RATE_LIMITEDmeans back off
You provide adapters for storage (tokens, sync state, subscriptions). We provide Prisma and Supabase adapters, or you write your own.
Installation
npm install @svutil/callyQuick Start
import { Calendar } from "@svutil/cally";
import { PrismaTokenAdapter, PrismaSyncAdapter } from "@svutil/cally/prisma";
const cal = Calendar.microsoft({
clientId: process.env.MS_CLIENT_ID,
clientSecret: process.env.MS_CLIENT_SECRET,
adapter: new PrismaTokenAdapter(prisma),
syncAdapter: new PrismaSyncAdapter(prisma),
});
// After OAuth callback
await cal.auth.setTokens(userId, {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
});
// Initial sync - fetches everything, creates delta token
const result = await cal.sync.fullSync(userId);
// Later syncs - only gets changes
const changes = await cal.sync.incrementalSync(userId);Real-time Updates
For apps that need instant updates (meeting notes, scheduling tools):
// Create webhook subscription
await cal.subscriptions.ensure(userId, {
webhookUrl: "https://yourapp.com/webhooks/calendar",
});
// Handle incoming webhooks
app.post("/webhooks/calendar", async (req, res) => {
if (req.query.validationToken) {
return res.send(req.query.validationToken);
}
await cal.sync.incrementalSync(userId);
// Push to connected clients via websocket/SSE
res.status(202).send();
});
// Cron job - renew before expiry (subscriptions last ~3 days)
await cal.subscriptions.renewAll();API
Events
cal.events.list(userId, { startDate, endDate })
cal.events.get(userId, eventId)
cal.events.create(userId, { title, start, end, ... })
cal.events.update(userId, eventId, { title })
cal.events.delete(userId, eventId)
cal.events.listCalendars(userId)Sync
cal.sync.fullSync(userId); // Initial sync, creates delta token
cal.sync.incrementalSync(userId); // Uses delta token, only gets changesSubscriptions
cal.subscriptions.ensure(userId, { webhookUrl }); // Create or renew
cal.subscriptions.renewAll(); // For cron jobs
cal.subscriptions.delete(userId);Health
const health = await cal.health.check(userId);
// { isConnected, tokenExpiresAt, hasSubscription, lastSyncAt }Error Handling
import { CalendarError } from "@svutil/cally";
try {
await cal.events.list(userId);
} catch (error) {
if (error instanceof CalendarError) {
if (error.code === "REAUTH_REQUIRED") {
// Refresh token is dead, user needs to reconnect
}
if (error.code === "RATE_LIMITED") {
// Back off, retry after error.retryAfterMs
}
}
}Adapters
Prisma
import {
PrismaTokenAdapter,
PrismaSyncAdapter,
PrismaSubscriptionAdapter,
PRISMA_SCHEMA,
} from "@svutil/cally/prisma";
// Add PRISMA_SCHEMA to your schema.prismaSupabase
import {
SupabaseTokenAdapter,
SupabaseSyncAdapter,
SupabaseSubscriptionAdapter,
SUPABASE_SCHEMA,
} from "@svutil/cally/supabase";
// Run SUPABASE_SCHEMA as migrationCustom
interface TokenAdapter {
getTokens(userId: string): Promise<Tokens | null>;
saveTokens(userId: string, tokens: Tokens): Promise<void>;
deleteTokens(userId: string): Promise<void>;
}Production Checklist
For reliable calendar sync in production:
- Auto-sync on connect - Run
fullSyncright after OAuth, not manually - Webhook renewal cron - Run
subscriptions.renewAll()every 12 hours - Fallback sync cron - Run
incrementalSyncevery 15 minutes as safety net - Handle REAUTH_REQUIRED - Prompt users to reconnect when refresh tokens die
// Webhook handler
app.post("/webhooks/calendar", async (req, res) => {
if (req.query.validationToken) return res.send(req.query.validationToken);
await cal.sync.incrementalSync(userId);
res.status(202).send();
});
// Cron: every 12 hours
await cal.subscriptions.renewAll();
// Cron: every 15 minutes (belt and suspenders)
for (const userId of activeUsers) {
await cal.sync.incrementalSync(userId);
}License
MIT
