Self-Hosted Open Source as Agent Infrastructure

Run Vikunja and Google Calendar locally and your agent gets task management, scheduling, and reminders for $0/month — no SaaS, no vendor lock-in.

The Pattern

When you need your agent to manage tasks, check calendars, or send reminders, the instinct is to build it. Task CRUD, a database schema, reminder scheduling, maybe a webhook system. That’s weeks of work for table-stakes functionality.

The alternative: install a self-hosted open-source tool that already does it, and point your agent at its API.

@Obi runs on a Mac Mini and talks to two self-hosted services:

  • Vikunja — task management with projects, priorities, due dates, labels, reminders, and a full REST API
  • Google Calendar — event scheduling via the googleapis client library with OAuth2

The agent doesn’t implement task management. It’s a conversational interface on top of infrastructure that already works.

Why Self-Hosted

The economics are stark. Building equivalent functionality from scratch means:

  • Task CRUD with priorities, due dates, labels
  • Recurring task support
  • Reminder scheduling and delivery
  • Per-user auth (if you ever add users)
  • Webhook integrations
  • A UI for manual edits

Using SaaS APIs means $30-50/month for Todoist + calendar + reminder services, plus vendor lock-in and rate limits.

Self-hosting Vikunja on the same Mac Mini that runs the agent: $0/month, localhost latency, full data ownership, and an API that’s richer than most SaaS offerings.

The Module Pattern

Each capability is a module that exposes one standard method:

// modules/tasks.js
export function getMorningBriefSummary() {
  const tasks = await fetchVikunjaTasksDueToday();
  const suggested = prioritizeTasks(await fetchAllOpenTasks());
  return formatTaskBrief(tasks, suggested);
}

// modules/calendar.js
export function getMorningBriefSummary() {
  const events = await getTodayEvents();
  return formatCalendarBrief(events);
}

The orchestrator doesn’t care what’s inside each module. It collects summaries and posts them:

// scheduler.js
const BRIEF_HOUR = 6;
const BRIEF_MINUTE = 30;
let briefPostedToday = false;

setInterval(async () => {
  const now = new Date();
  if (now.getHours() === BRIEF_HOUR && now.getMinutes() === BRIEF_MINUTE && !briefPostedToday) {
    const sections = await Promise.all(
      modules.map(m => m.getMorningBriefSummary())
    );
    const brief = sections.filter(Boolean).join('\n\n');
    await sendToDiscord(brief);
    briefPostedToday = true;
  }
  // Reset at midnight
  if (now.getHours() === 0 && now.getMinutes() === 0) {
    briefPostedToday = false;
  }
}, 60_000);

Adding a new data source to the morning brief means implementing one method. The orchestrator handles assembly.

Vikunja Task Prioritization

The morning brief doesn’t dump every open task. It selects a prioritized subset — enough to be actionable, not so many that you ignore the list:

async function getSuggestedTasks() {
  const tasks = await fetchAllOpenTasks();

  const urgent = tasks
    .filter(t => t.priority >= 4)
    .sort((a, b) => (a.dueDate || Infinity) - (b.dueDate || Infinity))
    .slice(0, 3);

  const soon = tasks
    .filter(t => t.priority >= 2 && t.priority < 4 && !urgent.includes(t))
    .sort((a, b) => (a.dueDate || Infinity) - (b.dueDate || Infinity))
    .slice(0, 2);

  const backlog = tasks
    .filter(t => t.priority < 2 && !urgent.includes(t) && !soon.includes(t))
    .slice(0, 1);

  return [...urgent, ...soon, ...backlog]; // max 6
}

Three urgent, two soon, one backlog. The user gets a focused list every morning without having to triage manually.

Google Calendar: OAuth2 Desktop Flow for Headless Agents

Google’s OAuth2 has a specific flow for desktop/headless applications. The key difference from web apps: you use a “Desktop App” credential type, and the first authorization happens through a local redirect.

import { google } from 'googleapis';
import { readFileSync, writeFileSync, existsSync } from 'fs';

const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
const TOKEN_PATH = './config/google-token.json';

function getAuthClient() {
  const { client_id, client_secret } = JSON.parse(
    readFileSync('./config/google-credentials.json', 'utf-8')
  ).installed;

  const auth = new google.auth.OAuth2(client_id, client_secret, 'http://localhost:3000/callback');

  if (existsSync(TOKEN_PATH)) {
    auth.setCredentials(JSON.parse(readFileSync(TOKEN_PATH, 'utf-8')));
    auth.on('tokens', (tokens) => {
      // Auto-save refreshed tokens
      const current = JSON.parse(readFileSync(TOKEN_PATH, 'utf-8'));
      writeFileSync(TOKEN_PATH, JSON.stringify({ ...current, ...tokens }));
    });
    return auth;
  }

  // First run: generate auth URL, user visits it, paste the code
  const url = auth.generateAuthUrl({ access_type: 'offline', scope: SCOPES });
  console.log('Visit this URL to authorize:', url);
  // ... handle the callback, save tokens to TOKEN_PATH
}

The setup:

  1. Create a “Desktop App” OAuth credential in Google Cloud Console
  2. Download the credentials JSON
  3. Run the auth script once — it opens a browser for consent
  4. The script saves the refresh token locally
  5. From then on, the agent uses the refresh token to auto-refresh access tokens — fully headless
async function getTodayEvents() {
  const auth = getAuthClient();
  const calendar = google.calendar({ version: 'v3', auth });

  const startOfDay = new Date();
  startOfDay.setHours(0, 0, 0, 0);
  const endOfDay = new Date();
  endOfDay.setHours(23, 59, 59, 999);

  const res = await calendar.events.list({
    calendarId: 'primary',
    timeMin: startOfDay.toISOString(),
    timeMax: endOfDay.toISOString(),
    singleEvents: true,
    orderBy: 'startTime',
  });

  return res.data.items || [];
}

Key Takeaway

Your agent doesn’t need to be a task manager, a calendar app, or a reminder system. It needs to talk to things that already are. Self-hosted open-source tools give you production-grade backends at $0/month with full API access and data ownership. The agent’s job is the conversational layer — understanding intent, routing to the right service, and presenting results. Let proven infrastructure handle the rest.