personal
Linkubit
One inbox for every channel a customer might message you on, with an agent that answers and knows when to stop.

Most businesses I know sell through conversations, and those conversations are scattered. A customer messages on WhatsApp, follows up by email, then replies to an Instagram ad, and to the business those are three strangers. Linkubit is my attempt at fixing that: one customer graph underneath every channel, so a person stays one person no matter where they arrive.
The part that was harder than the agent
I expected the interesting problem to be the AI. It wasn’t. The hard part was tenancy.
In a system where one company’s messages must never appear in another’s inbox, route-level permission checks are the wrong place to enforce it. Every new endpoint is another chance to forget. So isolation lives in the database: row-level security is enabled and forced on every tenant-scoped table, and a test derived from the live schema fails the build when a new table appears without a policy.
- 34
- Alembic migrationsevery tenant table policy-checked
- 1,300+
- end-to-end scenariosrun against real PostgreSQL, not mocks
- 8
- Terraform modulesone estate, no console clicking
That test caught two defects I would never have found by reading code. The first:
a superuser bypasses row-level security even when it is FORCEd, so the policy
looked correct in schema inspection and did nothing in practice. The second was
worse and subtler. current_setting returns an empty string rather than NULL on
a recycled connection, so a pooled connection that had lost its tenant context
did not fail closed the way I assumed it would. It silently matched nothing,
which looks identical to “this tenant has no data” until the day it doesn’t.
Safety rules belong in code
The agent is a small LangGraph loop over Gemini: load context, run a ReAct loop with seven tools, persist and reply. Seven tools, not seventy — the reference codebase I started from had a single 8,810-line tools file, and I wanted each tool to map to one module I could reason about.
The design decision I would defend hardest is that none of its guardrails are in
the prompt. There is a gate before any tool runs, a can_send check on every
outbound message that tests consent, suppression, domain verification and pause
state, and a verifier after generation that refuses any reply claiming to be
human. A prompt that says “never claim to be a person” is a suggestion. A
function that inspects the generated text and refuses to send is a rule.

What I would do differently
I built the console before I had anything real flowing through it, which meant I designed screens for data shapes that later turned out to be wrong. The inbox survived that; the campaign screens were rebuilt twice.

Linkubit is deployed to staging. It is not in production and I am not going to describe it as though it were: SES production access and Meta app review are both still outstanding, and those are calendar problems rather than code problems.