bruno.exePROJECTScontact.exe
CONTROL PLANE

</control plane>

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

PROBLEMA 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.
SYSTEMOne 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.
RESULTIn 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 APPROVESEvery write the agent proposes, and every email, which needs a single-use token bound to the exact draft.

SPECIFICATIONS

ROLESOLO BUILD
DATEAug 2026 to present
TYPEINTERNAL TOOL · WEB · macOS
STATUSIN DAILY USE
STACKnext.js · typescript · postgres · supabase · row level security · pgvector · vercel cron · claude agent sdk +4 more
LINKSnone public
AVAILABILITYprivate, in daily use (live totals below)

This is a small internal tool for my own work, not a product.

== WHAT IS THIS ==

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.

== </stack and architecture> ==

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.

== </how it fits together> ==

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.

SOURCESGoogle Calendar 2-way, pushGmail, read-only scopeObsidian vault via GitHubgit trailers close tasksClaude Code sessionsJJ MARINE ENQUIRIESbearer secret, constant-timezod-checked bodydeduped on the lead idfiled as a task + pushCONTROL PLANE · NEXT.JSon Vercel, 61 API routes20 cron jobs, 26 schedulesruns recorded, idle flaggedAsk agent: 34 tools, zod inputwrites wait for a tappublic /api/status, totalsSUPABASE POSTGRES · hq58 migrationsowner-only RLS, anon revokedsync state per cron, per feedfull-text + pgvector searchCLIENTSweb app (PWA)macOS app, WKWebView7 WidgetKit widgetsMCP server for Claude Codethis site: System MonitorAPPROVALdrafts and a review queueone-time 15 min token,bound to the draft hashnothing sends on its ownsync, ingestwebhookRLSread, actproposeSOURCESGoogle Calendar 2-way, pushGmail, read-only scopeObsidian vault via GitHubgit trailers close tasksClaude Code sessionsJJ MARINE ENQUIRIESbearer secret, constant-timezod-checked bodydeduped on the lead idfiled as a task + pushCONTROL PLANE · NEXT.JSon Vercel, 61 API routes20 cron jobs, 26 schedulesruns recorded, idle flaggedAsk agent: 34 tools, zod inputwrites wait for a tappublic /api/status, totalsSUPABASE POSTGRES · hq58 migrationsowner-only RLS, anon revokedsync state per cron, per feedfull-text + pgvector searchCLIENTSweb app (PWA)macOS app, WKWebView7 WidgetKit widgetsMCP server for Claude Codethis site: System MonitorAPPROVALdrafts and a review queueone-time 15 min token,bound to the draft hashnothing sends on its ownsync, ingestenquiry webhookRLSread, acta send needs approval
Control Plane on 22 Oct 2026. Every box is a part of the repo; counts are from main.

== </how it works> ==

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.

== </key decisions> ==

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.

== </running now> ==

System Monitor · hq.brunojaamaa.dev

Reading the control plane…

== </the problem> ==

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.

== </the approach> ==

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.

== </the story> ==

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.

== </where it doesn’t work> ==

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.

== </what was learned> ==

>

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.

next:

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.