arrow_back BACK DISCUSS_THIS
// case-study.md N°.003 · OSS

The silent
guardian
of your
calendar.

Alfred is a self-hosted, single-user AI executive assistant. Natural-language tasks in; scheduled calendar blocks out. Two-way Google Calendar sync, Telegram chat, and a pluggable LLM backend. Open source.

OSS · MIT · SHIPPED 2025 · SELF-HOSTED · READ ≈ 3 MIN
§01 problem

The list is the lie.

Every productivity tool I've used asks me to do the hard part myself: decide what gets scheduled when. The list grows. The calendar stays empty. The list grows again.

A task without a calendar block isn't a task. It's a wish.

Alfred is what I wanted: type a task in natural language, get it scheduled into the next available focus block on my real calendar, with a Telegram nudge before it starts. No SaaS, no team features, no kanban view. One user, one calendar, one assistant.

§02 loop

Five verbs.

The whole product is five verbs, looped. Everything else is configuration.

mic capture

Capture.

Tasks in natural language. Type them, speak them, forward a Telegram message.

category organize

Organize.

Schedule onto the calendar based on priority and deadline.

notifications remind

Remind.

Telegram pings before a block starts.

calendar_month plan

Plan.

Daily schedule generated for review each morning.

sync sync

Sync.

Two-way with Google Calendar. The calendar is the source of truth.

§03 architecture

The runtime.

One container. Two surfaces (web + Telegram). One agent loop in the middle. Google Calendar is the source of truth; everything else syncs.

// SURFACE Browser web UI // SURFACE Telegram chat parity // SELF-HOSTED · docker-compose up // FRONTEND React 19 Vite · Tailwind 4 // BACKEND · FastAPI REST · /tasks · /events Agent loop · tool-calling GCal adapter · 2-way sync Telegram bot Scheduler · check-ins // STATE SQLite tasks · sessions events · tokens SQLAlchemy async // PLUGGABLE LLM Claude OpenAI Ollama (local) one adapter, three backends // SOURCE OF TRUTH Google Calendar OAuth 2.0 · webhook + 15-min poll fallback user input agent action pluggable / async
// scope

One container, one user.

No multi-tenancy. No teams. Single SQLite file. docker-compose up on a $5 VPS and you own it.

// LLM-agnostic

Bring your own brain.

Claude tested across the full agent loop. OpenAI + Ollama on basic chat. Choose by latency, cost, or privacy.

// truth

GCal wins.

Google Calendar is the source of truth. Alfred is a thin layer that decides what to put there.

§04 engineering

Three hard parts.

Agent loop, calendar sync, and surface parity. Everything else is plumbing.

// §04.a

Natural language → calendar block.

"Move the Tuesday gym slot to 7am and add a 2-hour deep work block before lunch" hits the agent loop, which has four tools: find_free_slot, create_event, reschedule_event, and set_reminder.

The LLM picks tools, executes them, observes results, and keeps looping until either the task is fully scheduled or it escalates back to me with a clarifying question. No hard-coded intents — the model picks the shape of each request.

// USER "move gym to 7am..." LLM tool select find_free_slot() create_event() reschedule_event() set_reminder() observe → iterate DONE // FIG. 04.A — AGENT LOOP
// §04.b

Two-way sync without conflicts.

Google Calendar push notifications via webhook for fast incoming changes; a 15-minute poll as belt-and-suspenders for missed webhooks. Outgoing changes write to Alfred's SQLite first, queue a GCal API call, and reconcile on response.

Conflict rule is simple: most-recent-update wins, with the GCal etag as the version cursor. Edge cases (offline edits, dropped webhooks) get caught by the poll.

// FIG. 04.B — SYNC TIMELINE
ALFRED GCAL write queue · API call stored external edit webhook applied poll · t+15m catches missed // FIG. 04.B — WEBHOOK + POLL FALLBACK
// §04.c

Two surfaces, one agent.

The web UI and the Telegram bot are both thin shells over the same agent loop. Same tool registry, same context window, same SQLite session table. You can start a conversation on web, walk away, finish it on Telegram, and Alfred doesn't notice.

Built as a session-key abstraction — both the browser and the Telegram chat resolve to the same user_id, and conversation state is loaded by user, not by surface.

// FIG. 04.C — SHARED CONTEXT
# web request
POST /chat
session: browser_xyz
user_id: u_42
message: "what's left for today?"

# minutes later · telegram
POST /chat
session: telegram_abc
user_id: u_42
message: "actually push the call to 4pm"

# context loaded by user_id, not session
context = db.sessions.find({
  user_id: "u_42",
  $orderby: { updated: -1 }
})

# → agent sees the morning thread + the new message
 reschedules the call · replies via Telegram
§05 stack

Open source, all the way down.

The repo's on GitHub. Anyone can read the architecture; some folks have already forked. Stack chosen so you can self-host on a laptop or a $5 VPS.

alfred/ · tree -L 2 MIT
alfred/
├── backend/            # Python 3.11 · FastAPI · async
│   ├── api/          # REST endpoints
│   ├── agents/       # pluggable LLM tools
│   ├── integrations/ # gcal + telegram adapters
│   └── models/       # SQLAlchemy · SQLite
├── frontend/           # React 19 · Vite · Tailwind 4
│   ├── src/components/
│   ├── src/hooks/
│   └── vite.config.ts
├── docker/             # self-hosting
├── README.md
└── LICENSE
BACKEND
FastAPI
DB
SQLite
ORM
SQLAlchemy
FRONTEND
React 19
LLM
Claude · OpenAI · Ollama
I/O
GCal · Telegram
§06 lessons

"Build for yourself first. The internet of one is a real market."

— me, every weekend

Alfred is what happens when you stop trying to build a startup and start trying to fix your own week. Single user, no roadmap, no growth metric — just the smallest possible thing that makes the next Monday feel less heavy.

I'd recommend every founder ship something like this every year. It calibrates your taste in a way SaaS revenue doesn't.

Open-sourcing it was the right call. Other people self-hosting Alfred has surfaced bugs I'd never have caught alone, and the contributions have made it better than the version I would have shipped solo.

The pluggable LLM backend — Claude, OpenAI, or Ollama — was driven by contributors who wanted to run everything offline. Good problem to have.

// install

Clone the repo. Run docker-compose. You're done.