Spotify Pomodoro
- Overview
A self-hosted pomodoro timer with Spotify integration and overtime mode that respects your flow state. Built with Effect-TS functional programming throughout, demonstrating production-grade architecture patterns with type-safe services, schema validation, and composable layers.
- Technologies
- Effect-TSAstroSpotify APISQLiteOpen SourceDocker
A focus timer that respects your flow
Most pomodoro timers interrupt you at the worst moment. Spotify Pomodoro features overtime mode that lets you keep working when you're in the zone, tracking extra time without forcing a break.
When your timer hits zero, it keeps counting up (+MM:SS) instead of interrupting. You decide when to take a break.
Full functional programming with type-safe services, schema validation, and composable layers.
Your productivity data stays on your machine. SQLite database, Docker deployment, optional auth.
Code Highlights
Clean, type-safe implementations that prioritize developer experience and maintainability.
Computed properties that handle the transition from countdown to count-up seamlessly.
get isOvertime(): boolean {
return this.remainingSeconds <= 0
&& this.status === "running";
}
get displayTime(): string {
const totalSeconds = Math.abs(this.remainingSeconds);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const sign = this.isOvertime ? "+" : "";
return `${sign}${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}Dependency injection with compile-time tracking. Services expose streams for reactive state updates.
export class Timer extends Effect.Service<Timer>()("Timer", {
effect: Effect.gen(function* () {
const stateRef = yield* SubscriptionRef.make(initialTimerState);
const start = Effect.gen(function* () {
// Pure functional timer logic with Effect streams
});
return {
start,
pause,
reset,
changes: stateRef.changes
};
}),
accessors: true,
}) {}Dynamic two-note chime generated at runtime. More pleasant than static beep files.
const playChime = Effect.sync(() => {
const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain).connect(ctx.destination);
osc.frequency.setValueAtTime(880, ctx.currentTime);
osc.frequency.setValueAtTime(660, ctx.currentTime + 0.15);
gain.gain.linearRampToValueAtTime(0.9, ctx.currentTime + 0.02);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
osc.start();
osc.stop(ctx.currentTime + 0.5);
});Effect Services Architecture
The application uses Effect-TS's service pattern for clean dependency injection. Each service encapsulates a specific concern and can be provided with mock implementations during testing.
Layer Composition
React Components
App, TimerDisplay, StatsDialog, PlaylistSelector
Effect Runtime
Browser singleton with useTimer hook bridge
Service Layer
Timer, SpotifyAuth, SpotifyClient, Audio, SessionRepo
Astro API Routes
REST endpoints for session persistence
SQLite + Drizzle
pomodoros, focusSessions, breakSessions
Effect Services
Timer
Core timer logic with overtime
SpotifyAuth
OAuth with PKCE flow
SpotifyClient
Playback control API
Audio
Web Audio synthesis
SessionRepo
Database operations
Why Effect-TS? Compile-time dependency tracking, type-safe error handling with discriminated unions, and composable layers that make testing straightforward. Each service declares its dependencies explicitly.
Features & Controls
Every action is accessible without reaching for the mouse. Perfect for developers who prefer keyboard navigation.
Comprehensive analytics computed from raw session data. No pre-aggregation needed—all metrics calculated on demand.
Streaks
Current and longest streak tracking across consecutive days
Time Windows
Stats filtered by today, week, month, and year
Overtime Tracking
See how much extra time you worked beyond configured limits
Tech Stack
Frontend
Backend
Infrastructure
Related notes

Kavita to Obsidian
An Obsidian plugin that syncs reading annotations from Kavita into your vault as searchable markdown. Built with Effect-TS for type-safe error handling, comprehensive testing with 80% coverage, and professional OSS practices including semantic versioning, changelogs, and stress testing for 10,000+ annotations.

Take the Keys Away From Your Agent
A process that runs model-authored code should hold no credential worth stealing and have no route to anywhere it could send one. Builds that from the bottom up: what TCP, HTTP and TLS actually are, how certificate authorities decide who you believe, and what a proxy can and cannot see. Ends with an interactive walkthrough of one request through every process that touches it, which you can rerun on a flat network to watch the whole thing become irrelevant.

Schema.Redacted Across the HttpApi Wire
Schema.Redacted and Schema.RedactedFromValue are encode-forbidden by design — so they cannot be wire fields on either side of HttpApi. The reference repo demonstrates the failure modes with four endpoints, pins the exact encoder error messages in a backend test, and shows the working pattern: plain validated wire values, Redacted.make on receipt, Redacted.value at exactly one audit boundary inside the SQL repo.
Get in touch
Have a question about something I wrote, or want to talk? Send me a message.