Skip to content
Back to Notes
Open SourceTechnology2 min read

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.

Overtime Mode

When your timer hits zero, it keeps counting up (+MM:SS) instead of interrupting. You decide when to take a break.

Effect-TS Architecture

Full functional programming with type-safe services, schema validation, and composable layers.

Self-Hosted First

Your productivity data stays on your machine. SQLite database, Docker deployment, optional auth.

Implementation

Code Highlights

Clean, type-safe implementations that prioritize developer experience and maintainability.

Overtime Mode

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")}`;
}
SchemaComputed PropsUX
Effect Service

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,
}) {}
Effect.ServiceSubscriptionRefStreams
Web Audio Synthesis

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);
});
Web Audio APIOscillatorEnvelope
System Design

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.

User Experience

Features & Controls

Keyboard-First Controls

Every action is accessible without reaching for the mouse. Perfect for developers who prefer keyboard navigation.

Space / EnterStart timer
BSkip to break (during focus)
FSkip to focus (during break)
EEnd session early
RReset timer
Rich Statistics

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

Technology

Tech Stack

Frontend

Astro 5SSR with React islands
React 19Interactive components
Tailwind CSS v4Utility-first styling
shadcn/uiRadix-based components

Backend

BunFast JS runtime
Effect-TS 3.xFunctional programming
SQLiteEmbedded database
Drizzle ORMType-safe queries

Infrastructure

DockerContainerization
CoolifySelf-hosted deployment
VitestUnit testing
BiomeLinting & formatting
Keep reading

Related notes

Open Source
Technology

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.

Effect-TSTypeScriptObsidian+3
The Innovation LabRead
Article
Technology

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.

SecurityNetworkingTLS+3
The Innovation LabRead
Article
Technology

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.

Effect-TSSchema.RedactedHttpApi+3
The Innovation LabRead

Get in touch

Have a question about something I wrote, or want to talk? Send me a message.