</control plane>
A small internal tool for my own work, where I try patterns before client systems.
20
scheduled jobs, every run recorded
34
agent tools, inputs schema-checked
2-way
Google Calendar sync, push and echo guard
1 tap
needed before anything is written or sent
IN SHORT
| PROBLEM | A small internal tool for my own work: my calendar, mail, notes and tasks were in five apps, and I wanted one place to try patterns before using them in client systems. |
|---|---|
| SYSTEM | One Postgres schema synced with my calendar, mail and notes, 20 scheduled jobs that record every run, and an agent with 34 schema-checked tools that proposes changes instead of making them. |
| RESULT | In daily use (private). The calendar syncs both ways on push, mail and notes land in one search with a source on every answer, and every job run is recorded. |
| A PERSON APPROVES | Every write the agent proposes, and every email, which needs a single-use token bound to the exact draft. |
SPECIFICATIONS
| ROLE | SOLO BUILD |
|---|---|
| DATE | Aug 2026 to present |
| TYPE | INTERNAL TOOL · WEB · macOS |
| STATUS | IN DAILY USE |
| STACK | next.js · typescript · postgres · supabase · row level security · pgvector · vercel cron · claude agent sdk +4 more |
| LINKS | none public |
| AVAILABILITY | private, in daily use (live totals below) |
This is a small internal tool for my own work, not a product.
Control Plane is a small internal tool Bruno Jaamaa keeps for his own work, and the place he tries patterns (approval before sending, search with cited sources, safe retries) before using them in client systems. It syncs his calendar and email, keeps his tasks and notes in one place, measures his own output, and runs small agents that draft and propose work for him to approve. Underneath it is a Next.js app over Supabase Postgres that syncs Google Calendar both ways, ingests Gmail read-only, indexes his notes and files for cited search, and runs 20 scheduled jobs and a Claude agent with 34 tools on top. Anything the agent would write or send is proposed first and waits for a tap. It is private by design, so this page shows how it is built and live totals, never what is inside it.
next.js · typescript · postgres · supabase · row level security · pgvector · vercel cron · claude agent sdk · mcp · google calendar api · gmail api · swiftui
Next.js 16 and React 19 on Vercel (Sydney region), Supabase Postgres with row level security forced on every table, the Claude Agent SDK for the agent, a stdio MCP server for Claude Code, and a SwiftUI macOS shell with WidgetKit widgets. Writes from the browser carry a client-minted idempotency key and land with ON CONFLICT DO NOTHING. Model use runs on the Claude plan by default; the pay-per-token API is used only when explicitly switched on, never as a silent fallback, and embeddings run on a free edge function. 160 test files cover the sync, ranking, approval and gating logic.
Google Calendar (two-way, with push notifications), Gmail through a read-only scope, the Obsidian vault through GitHub, git commit trailers and Claude Code sessions feed a Next.js app on Vercel with 61 API routes and 20 cron jobs on 26 schedules, each run recorded. JJ Marine website enquiries arrive at an ingest route that checks a bearer secret, validates the body and ignores duplicates by lead id. The app stores everything in a Supabase Postgres schema with 58 migrations and owner-only row level security, with full-text and vector search. It is read by the web app, a native macOS app with seven widgets, an MCP server for Claude Code, and this site through a public totals-only status endpoint. The Ask agent has 34 tools; anything that writes or sends is proposed first, and a send needs a one-time approval token bound to the draft.
Data model and auth
A single hq schema in Supabase Postgres, 58 migrations. Row level security is forced on every table with owner-only policies, and the anonymous role has no grants at all.
Two-way calendar sync
Google Calendar push channels hit a webhook that checks the channel token and resource id. An echo guard drops changes the app itself made, and a stale etag gets one retry, never a blind overwrite.
Mail and notes ingest
Gmail is read through a read-only scope every hour. The Obsidian vault arrives through GitHub twice an hour and is chunked into a search index, skipping chunks whose hash has not changed.
Cited search
Postgres full-text and a pgvector lane (384-dimension embeddings from an edge function) fused with reciprocal rank fusion. Every hit returns its source path, so an answer can be checked. With no embeddings it falls back to full text.
An agent that asks first
34 tools, 16 read and 18 write, each input validated with zod. A write becomes a proposal card and runs only after a tap. Requests are capped at 60 messages, 4,096 output tokens and 8 turns.
Approval before any send
Agents can only draft mail. Sending needs a single-use token, stored as a hash, bound to the draft id and its content hash, valid for 15 minutes. Change one word and the token is void.
Scheduled work that proves it ran
20 cron jobs on 26 schedules. Each is wrapped so it records its run and its error in a sync-state row; a job that answers 200 and moves nothing is counted as idle, and a long idle streak flags the connector.
Integrations in and out
JJ Marine website enquiries arrive at an ingest route with a constant-time bearer check and are deduped on the lead id, so a retry can never file a lead twice. Claude Code reaches the same tools through an MCP server over stdio.
Clients
A web app, a native macOS app that wraps it in a WKWebView with seven WidgetKit widgets, and an app-only gate so the deployed site only answers the Mac app or an unlocked browser.
DECISION 01
Never send without a tap. The agents can draft mail, posts and replies, but every send waits for a person, and the check is a token in the code path, not an instruction in a prompt. An agent that can send on its own will eventually send the wrong thing to the wrong person.
DECISION 02
Every answer carries its source. Search returns the note, file, message or event each hit came from, because an answer from a knowledge base is only useful if it can be checked.
DECISION 03
Totals in public, nothing else. The System Monitor on this page reads counts and timestamps from a public endpoint that is cached for five minutes and rate limited, so a later change cannot leak a task title or a calendar name by accident.
Reading the control plane…
This is a small internal tool for my own work, not a product. My work lived in five apps: calendar, mail, notes, tasks and a chat window that could not see any of them. An assistant is only useful if it can read the real data and act on it, and only safe if it cannot act without asking.
One Postgres schema as the source of truth, synced with the tools I already use, a scheduler that does the routine work and proves it ran, and an agent with typed tools that proposes changes instead of making them.
role & context
Solo build, from the first commit on 18 August 2026 to now. One user, private by design.
Control Plane is a small internal tool Bruno Jaamaa keeps for his own work. Day to day it syncs his calendar and email, keeps his tasks and notes in one place, measures his own output (a daily view of active hours from his Claude usage, tasks done, commits and lessons, with a simple score), and runs small agents that draft and propose work, which he approves before anything goes out. It is also where he tries patterns, such as approval before sending, search with cited sources and safe retries, before using them in client systems. It started in August 2026 as a task board and became one Postgres schema that his calendar, mail, notes, projects and agents all read and write.
Underneath it is a Next.js app on Vercel with 61 API routes over Supabase Postgres. Google Calendar syncs both ways every 15 minutes and on push notifications, Gmail is ingested read-only every hour, and the notes vault arrives through GitHub twice an hour and is indexed for search. Twenty scheduled jobs do the routine work, from backups to a morning plan, and each one records whether it actually moved data.
On top sits an agent. Ask is a Claude agent with 34 typed tools over the same data, so it can answer "what is due this week" with sources, or propose a task from a sentence. It never writes or sends on its own: writes wait for a tap, and mail can only leave with a one-time approval token bound to the exact draft. Claude Code reaches the same tools through an MCP server.
It is one of several knowledge bases he has built. The same shape runs at JJ Marine, where he is Technical Lead: website enquiries arrive here by webhook and become tasks, and the internal assistant he built there, now live, answers from the company's own knowledge base and runs tasks only after a person approves them.
It has one user and is built for one. There are no evals for the agent's answers yet, only tests on the code around it; the vector search lane is new (October 2026) and search falls back to full text where embeddings are missing; and logging is plain server logs, with no tracing service. Some agents still need the laptop open until they move to a home PC, and there is nothing here a stranger can open: the evidence is the diagram and the live totals, not a demo.
A cron that returns 200 is not proof that it did anything. Record what it moved.
Idempotency belongs on the receiving side. The sender will retry; make a duplicate harmless.
Put the approval in the control flow, not the prompt. A model cannot talk its way past a token it does not have.
Move the agents onto my home PC so nothing needs the laptop open, and put the JJ Marine agent and the control plane on one approval queue.
IN DAILY USE