System map · the complete set

Everything an agency knows about a client, in one place

Eight maps of the same machine, stacked end to end. What we hold on a client and where the gaps are, how a review or a phone call finds its way in, how a post gets made and sent back out, and what it all looks like running at once.

01

Everything we know. And everything we do not.

A single page per client showing every source the agency has connected, every source it has not, and exactly how much has actually come through. Three clients here, at three different stages of onboarding.

standalone version →

an illustrative view — the clients below are invented

The shape underneath

Five layers, split by what the data is rather than by which vendor it came from. Every row in every layer carries the client it belongs to, and the database itself refuses a read or a write that crosses that line.

L0registry

Who the business is

Name, address, phone, service area, timezone, brand palette, and where its site and files live. Small, hand-curated, and the row everything else hangs off.

tenantsone row per client
Postgresthe anchor row
L1connections

The keys to their accounts

One credential per thing the client already owns — their listing, their page, their ad account, their CRM. Encrypted at rest and decrypted only at the moment of the call. Never stored in a repo.

connectionsencrypted
Postgresencrypted column
L2facts

Everything that happened

One table for every synced fact — reviews, ad numbers, conversations, form fills, visits. A kind column tells them apart. Plus the identity graph, because the same person is a CRM contact and a Facebook commenter and a Google reviewer, with three different IDs.

factspeopleidentities
Postgresstructured + raw
L3blobs

Every original file

Before-and-afters, phone video, raw uploads, logo files. The bytes never go in the database and never go in git — the row holds a bucket and a key and nothing else.

assetspointers only
Object storage~66 MB a job
free to serve
L4meaning

What a model actually reads

Text chunks with their embeddings sitting in the same database as the rows they describe, so one query can filter by client, filter by rating, and search by meaning at the same time. Plus the compiled brief a person can review before it ships.

chunksvector indexcontext_documents
Postgres + gitvectors beside
their own rows
—audit

What was done, and by what

Append-only. Every sync, every generated draft, every write. This is what lets you change your mind about the structure later without losing the history.

eventsappend-only
Postgresnever updated
the rule that lets it evolve Every synced row keeps its untouched payload. Typed columns are a projection of that payload, never a replacement for it.

When a platform adds a field, or you decide six months in that you want sentiment on every conversation, you add a column and backfill from what you already stored. No re-fetch, no rate limits, no data you threw away because it did not fit the schema you had on the day you wrote it.

now — what is actually in it
live — rows present empty — connected, never pulled missing — not connected

bright · tenant

Bright Exteriors

Fort Collins, CO · no phone ·

0%0 of 13
L0

Identity and NAP

empty tenants
Legal
Bright Exteriors LLC
Phone
not captured
Location
Fort Collins, CO
Timezone
America/Denver
Email
not captured
L0

Brand board

missing tenants.brand

No palette captured. Pull it from the site stylesheet or the brand kit.

L1

Website and GitHub

empty connections

Nothing linked.

L2

Google Business Profile

missing facts kind=gbp_snapshot

No connection for this yet.

L2

Google reviews

missing facts kind=review

No connection for this yet.

L2

Facebook page

missing facts kind=post

No connection for this yet.

L2

Ad performance

missing facts kind=ad_metric

No connection for this yet.

L2

Conversations and contacts

missing facts kind=message

No connection for this yet.

L2

Form submissions

missing facts kind=form_submission

No connection for this yet.

L2

Site visits

missing facts kind=site_visit

No connection for this yet.

L2

Keyword research

missing facts kind=keyword

No connection for this yet.

L3

Job photos and video

missing assets

No connection for this yet.

L4

Embeddings

empty chunks

Connected, but this has never been pulled.

handy · tenant

Handy Home Co.

Boise, ID · (555) 0142 · handyhome.example

46%6 of 13
L0

Identity and NAP

live tenants
Legal
Handy Home Co. LLC
Phone
(555) 0142
Location
Boise, ID
Timezone
America/Boise
Email
office@example.com
BoiseMeridianNampaEagle
L0

Brand board

live tenants.brand
primary#1F6FEB
ink#10243E
L1

Website and GitHub

live connections
githubagency/handyactive
websitehttps://handyhome.exampleactive
L2

Google Business Profile

empty facts kind=gbp_snapshot

Connected, but this has never been pulled.

L2

Google reviews

empty facts kind=review

Connected, but this has never been pulled.

L2

Facebook page

empty facts kind=post

Connected, but this has never been pulled.

L2

Ad performance

empty facts kind=ad_metric

Connected, but this has never been pulled.

L2

Conversations and contacts

live facts kind=message
1,962rows
Sources
crm
Newest
2026-08-26
most recentCan someone look at a water heater swap this week? The old one is leaking at the base.
L2

Form submissions

live facts kind=form_submission
199rows
Sources
website
Newest
2026-08-24
L2

Site visits

live facts kind=site_visit
1,358rows
Sources
website
Newest
2026-08-26
L2

Keyword research

empty facts kind=keyword

Connected, but this has never been pulled.

L3

Job photos and video

missing assets

No connection for this yet.

L4

Embeddings

empty chunks

Connected, but this has never been pulled.

summit · tenant

Summit Karting

Reno, NV · (555) 0188 · summitkarting.example

100%13 of 13
L0

Identity and NAP

live tenants
Legal
Summit Karting LLC
Phone
(555) 0188
Location
Reno, NV
Timezone
America/Los_Angeles
Email
hello@example.com
RenoSparksCarson CityTruckeeWashoe County
L0

Brand board

live tenants.brand
primary#E5484D
asphalt#1C1F24
flag#FFB224
trim#0F7C74
L1

Website and GitHub

live connections
githubagency/summitactive
websitehttps://summitkarting.exampleactive
L2

Google Business Profile

live facts kind=gbp_snapshot
96rows
Sources
google
Newest
2026-08-26
L2

Google reviews

live facts kind=review
412rows
Sources
google
Newest
2026-08-25
most recentTook both boys for the nine-minute heats and they have not stopped talking about it. Staff walked us through the flag rules before we got on track.
L2

Facebook page

live facts kind=post
288rows
Sources
facebook
Newest
2026-08-24
most recentFriday night league standings are up. Two spots left in the intermediate bracket before we close entries.
L2

Ad performance

live facts kind=ad_metric
1,840rows
Sources
facebook
Newest
2026-08-25
L2

Conversations and contacts

live facts kind=message
6,217rows
Sources
crm
Newest
2026-08-26
most recentDo you do birthday parties for ten year olds, and is there a minimum height?
L2

Form submissions

live facts kind=form_submission
503rows
Sources
website
Newest
2026-08-25
L2

Site visits

live facts kind=site_visit
9,420rows
Sources
website
Newest
2026-08-26
L2

Keyword research

live facts kind=keyword
240rows
Sources
research
Newest
2026-08-01
L3

Job photos and video

live assets
3,140files in R2
Sources
Newest
—
L4

Embeddings

live chunks
11,208chunks
Sources
Newest
—
02

It is not a pipeline. It is a loop.

Everything a business touches flows into one brain — reviews, phone calls, text threads, job photos, what people did on the website. Agents read from it and publish back out to the same platforms the facts came from. And here is the part that makes it an ecosystem rather than a funnel: every post that goes out lands back in as a row, so next week the system already knows what it said.

standalone version →
What comes in Google reviews polled every morning also a place we post The profile itself hours, categories, area also a place we post Voice AI calls the whole transcript every call, as it ends Conversations texts and replies all day long Jobs and photos from Wayne's phone the moment he is done The website pages, forms, visits also a place we post client_context one row per business sealed per client Who reads it The job publisher every finished job you approve, then it goes The weekly agent mines the week's calls checks what it said before The dossier whole client, one page never goes stale You, just asking one question every source at once Where it goes Google Business Profile posts, photos, review replies the same place the reviews came from Facebook the business page what lands here comes back as a row The website a new section, deployed then read back in as a page YouTube the walkthrough clip views come back as a fact too every post that goes out lands back in the brain — that is why it never repeats itself
Wayne was great! Showed up on time and cleaned up after himself.
incoming call · 4m 12s
“Do you do drywall, or just demolition?”
text · 9:41 am
“Can you come Thursday instead?”
job 214 · 6 photos
Bathroom remodel — before and after
already posted · 9 days ago
“We also do drywall repair”
written from a real call
Drywall or demolition? Both, usually on the same job.
posted tuesday · coming home
kind='social_post' — now the system knows it said this

↔ swipe the diagram

fetched on a schedule arrives as it happens agents reading and publishing the return loop

The platforms are on both ends

Google is where the reviews come from and where the posts go. Same for Facebook and the website. The marks on those boxes are not decoration — the arrow genuinely goes both ways, which is the difference between a feed and an ecosystem.

Nobody hands anything off

The job publisher does not tell the weekly agent what it posted. It does not have to. Both write to the same table and both read the same table, so the handoff is the database rather than a message between two systems that have to stay in sync.

The loop is the whole point

A post that goes out and is forgotten is how you end up saying the same thing in week three and week eleven. Because published posts come back in as rows, "have we said this already" is a query, and next week's agent starts smarter than this week's did.

Six streams in, four readers, four places to publish, and one return path. None of the streams know about each other. A review, a phone call, a text thread, a photo from a driveway, a form submission, and a post that went out last Tuesday all land in facts as rows that differ only by their kind. That is the entire trick. Because they share one table, an agent writing Tuesday's post can read Monday's phone call, the service area, the brand voice, and every post already published — in one query, with none of it typed in twice.

03

Everything wired into one living database

Sources push and we pull — conversations, reviews, paid spend, organic performance. Replies and posts flow back out. One Postgres hub, sealed per tenant, feeding every human and AI reader from the same rows.

standalone version →
HighLevel conversations webhook · live Business Profile reviews ⇄ replies pub/sub · live Google Ads spend and clicks api pull · daily Facebook Ads spend and leads api pull · daily Search Console query performance api pull · daily PageSpeed scores over time api pull · daily SEO Rankings position tracking tracker · daily client_context postgres · pgvector · rls seal Dossier regenerated live AI callers read the brief first You, in chat one query, all of it Cloudflare R2 photos · video bytes GitHub site + compiled briefs kind='message' kind='review' ⇄ 'review_reply' kind='ad_metric' · google kind='ad_metric' · meta kind='gsc_query' kind='psi_score' kind='rank'
live push (webhook / pub-sub) scheduled pull flowing back out (replies, briefs, blobs)
04

A new review, and the trip it takes to get here

Somebody finishes a job happy and leaves five stars. That review lives on Google, not in our database — so once a day we go out and bring it home. Here is the whole trip.

standalone version →
A happy customer
writes five stars
posts it
New review
Google Business Profile
sits there until we fetch it
5:20 AM pull
client_context
postgres on the vps
reviews stored 910
Wayne was great!
Showed up on time, fixed it right, cleaned up after himself.
written by a person parked on Google pulled into the database

The pull runs once a day on a schedule, not the instant the review appears — so a morning review shows up the next time the job runs. It is safe to run over and over: each review is matched on its own id, so re-running updates what changed and adds what is new, and never creates a duplicate. Same trip for the profile itself — hours, categories, service area — which is why the database can answer what are your hours without anyone ever retyping them.

05

A phone call on Monday becomes a post on Tuesday

Nobody writes this post. A customer asks a good question on the phone, the answer gets stored, and two days later an agent goes looking for exactly that kind of moment and writes it up. Here is the whole loop.

standalone version →
Act one The call gets captured Monday, 9:14 AM
A customer calls
asks a real question
Voice AI answers
handles the whole call
Transcript
every word, written down
Workflow fires
posts it to the vps
The brain
lands as one row
transcripts 4748
incoming call · 4m 12s
“Do you guys do drywall, or just demolition?”
Act two The agent goes digging Tuesday, 3:00 PM
The prompt fires
on a schedule, not a whim
It reads the brain
who this business is
Mines the week
seven days of transcripts
Writes the post
from a real conversation
best moment found
4-minute call · a question people keep asking
ATH
All Things Handy
drafted Tuesday, 3:00 PM

Somebody called this week and asked do you do drywall, or just demolition?

Both. Usually on the same job.

She wanted a wall gone and did not know what was behind it. We spent a few minutes on the phone working out what was load bearing, what it costs to find out, and which parts she could handle herself.

If you have been staring at a wall for a year, that call is free.

What it pulled from the brain
Service area — where this can even be offered
Phone on file — the number that actually rings
Services list — so it does not promise plumbing
Brand voice — plain, no hype, never pushy

The interesting part is not the automation, it is the source material. A social post invented out of thin air sounds like it was invented out of thin air. A post built from a question a real person actually asked on the phone lands differently, because somebody else was already wondering the same thing. The transcripts sit in facts next to the reviews and the jobs, so the agent that writes the post is reading from the same brain that knows the service area and the phone number.

06

Two ways a post gets made, and the same four doors

One publisher runs off Wayne's camera roll — he finishes a job, uploads photos, and the system writes it up. The other wakes up once a week, goes through the phone calls, and finds something worth saying that nobody thought to write down. Both check the brain first so they never post the same thing twice.

standalone version →
Every job — automatic Wayne's phone photos from the driveway AI reads them before and after You click approve nothing posts until you do Every week — on its own The weekly agent wakes up on a schedule Goes through the calls what people kept asking Did we say this already? checks every past post client_context jobs · calls · every post ever made Publish same four doors every time Google Business Profile photos and updates Facebook the page, not the profile The website a new section, deployed YouTube the walkthrough clip
job 214 · 6 photos
Bathroom remodel — before and after
already posted · 9 days ago
“We also do drywall repair”
from a 4-minute call
Drywall or demolition? — nobody has written this one

↔ swipe the diagram

runs off a finished job runs off the week's phone calls out to all four

What the job publisher writes

minutes after the truck pulls away

Finished a full bathroom remodel in Springville this week. New tile, new vanity, and a floor that is finally level.

Six photos, before and after, in the comments.

Business ProfileFacebook WebsiteYouTube

What the weekly agent writes

tuesday, from a call nobody transcribed by hand

Somebody called this week and asked whether we do drywall or just demolition. Both, usually on the same job.

She wanted a wall gone and did not know what was behind it. That call is free.

Business ProfileFacebook WebsiteYouTube

The second publisher is the interesting one, and the check in the middle is why. Before it writes anything it reads every post that has already gone out, so week eleven does not quietly repeat week three. That check is only possible because the posts live in the same place as the calls and the jobs — a social_post row sits next to a call_transcript row, so "have we said this already" is a query rather than somebody scrolling back through a Facebook page trying to remember.

07

Two clients. Two brains. One Discord.

Every client gets a channel and a brain of its own. Somebody on the team types a question in a client's room and the answer comes back from that client's data — never from anyone else's. Multiply the whole picture by however many clients you have; the shape does not change.

standalone version →
One server, one room per client The agency server your team lives here # agency-ops your keys, your billing, your tools # all-things-handy wayne's room · his data only someone is typing… # go-kart-park same shape, different client # next-client # the-one-after-that add a room, add a brain. that is the whole onboarding. ath all things handy gkp go kart park row-level security between them one client's question cannot reach another client's rows Wayne's connections Google Business Profile reviews in · posts out his listing, his login Facebook Page a post goes out after every job page token, scoped to him Voice AI + the website calls in · new sections out his domain, his phone number Go Kart Park's connections Facebook Ads spend, reach, cost per lead rolls up every morning Google Business Profile a different listing entirely their login, not Wayne's The site + the blog a new post, deployed their domain, their repo
KKaylee · #all-things-handy
How did the ads perform over the weekend?
answered from wayne's rows only
Saturday and Sunday, steady. Cost per lead came down.
$41.20spend
7leads
$5.89per lead
facebook ads · rolling up
Weekend campaign — spend, reach, cost per lead
going out · facebook page
Bathroom remodel — six photos, before and after
going out · a new blog post
“What a birthday party actually costs” — now live

↔ swipe the diagram

Discord, one room per client All Things Handy Go Kart Park the seal between them

The room is the tenant

Which channel the question was typed in decides which brain answers. Nobody picks a client from a dropdown and nobody can forget to. Ask in Wayne's room, get Wayne's numbers.

Their keys, not yours

Each client's connections belong to that client — his listing, his page token, his phone number, his domain. The agency room holds the agency's own tools. The two never mix, which is what makes handing a client back to them a real possibility instead of a painful unwind.

Client three is a channel and a row

Nothing in this picture is per-client code. Adding a business means a new room and a new tenant row. The loaders, the agents, and the seal all work the same on day one as they do on client forty.

The interesting question is not how the answer gets written, it is how the system knows whose data to read. Here the channel carries the tenant. A question typed in #all-things-handy is scoped to Wayne before it reaches the database, and the database refuses to return anyone else's rows even if something upstream got it wrong — that is what the dotted line to the other brain means. SET LOCAL app.tenant_id is a belt, and row-level security is the suspenders.

08

Everything, arriving at once, and what comes back out

A review lands on Google. A customer calls and the voice AI takes it. Messages come and go all day. Wayne uploads photos from a driveway. All of it flows into one brain — and on Tuesday afternoon something reaches back in and writes a post out of it.

standalone version →
Google reviews what people said once a day Voice AI calls the whole transcript every call Conversations texts, calls, replies all day long Jobs and photos straight from the driveway as they happen kind='review' kind='call_transcript' kind='message' kind='job' client_context one row per business sealed per client The Tuesday agent mines a week of calls 3:00 pm, weekly The dossier whole client, one page always current You, just asking one question, every source any time writes the post renders the page answers in plain english
Wayne was great! Showed up on time and cleaned up after himself.
incoming call · 4m 12s
“Do you do drywall, or just demolition?”
pulled back out
7 days of calls · one question worth answering

↔ swipe the diagram

fetched on a schedule arrives as it happens uploaded from the field reading back out
ATH
All Things Handy
written Tuesday, 3:00 PM

Somebody called this week and asked do you do drywall, or just demolition?

Both. Usually on the same job.

She wanted a wall gone and did not know what was behind it. We spent a few minutes on the phone working out what was load bearing, what it costs to find out, and which parts she could handle herself.

If you have been staring at a wall for a year, that call is free.

Everything it had to know first
The call — what she actually asked
Service area — whether this can be offered
Services list — so it does not promise plumbing
Brand voice — plain, no hype, never pushy

Four streams in, three readers out, one brain in the middle. The streams do not know about each other and do not need to — a review, a phone call, a text thread, and a photo from a driveway all land in facts as rows with a different kind. That is the whole trick. Because they share a table, the thing that writes Tuesday's post can read a Monday phone call and the service area and the brand voice in one query, and none of it had to be typed in twice.