One governed platform for a company's internal AI automation

Published Sep 24, 2026Reviewed Oct 7, 2026

Bringing a company's internal automation tools (support operations, payment disputes, paid-social campaign building, assistant analytics) into one access-controlled dashboard and a set of Go services, built largely by AI coding agents under the same gates as people.

At a glance

Problem
Support, disputes, media buying and product teams each needed automation tools, and each tool rebuilt login, permissions, data views and admin screens, or fell back to scripts and direct database access.
Constraint
The tools touch customer data, ad campaigns and payment disputes, so access had to be governed in one place and production writes gated, while new tools still had to ship quickly.
What I built
One dashboard shell with a shared role registry, reusable UI and data-view packages, and small Go services for dispute evidence and workflow utilities, on a Go kernel with one-way dependencies and a validated service manifest.
Result
New products inherit access control, tables and deployment instead of rebuilding them. About 1,900 merges came from AI coding agents' branches, most of them within one month, all through the same gates.

Overview

The company runs several consumer subscription products. Behind them sit internal teams that live in automation: support operators supervising an AI agent, a disputes team answering chargebacks, media buyers building ad campaigns, and a product team reviewing conversations with an in-app AI assistant. Handing those systems to operators raw, through database consoles, warehouse tabs and scripts, is too dangerous.

Without a shared home, every team builds its own internal tool and repeats the same work: authentication, authorization, tables, filters, admin screens, deployment. As the tools multiply, the way the company operates and trusts its automation fragments with them. The approach was to treat every internal automation surface as a governed product inside one dashboard shell, so a new product inherits the shell instead of rebuilding it.

One access registry for every product

Every product is declared once in a role table with a viewer role pool and an optional admin pool. Products without a writer tier have no admin pool. When one exists, it must be a strict subset of the viewer pool, and a test locks that invariant. Server procedures are built from the table, so a product's API cannot exist without its access rule.

// lib/auth/product-roles.ts: pure data, safe to import from the browser
disputes: {
  displayName: "Disputes",
  viewer: ["ADMIN", "DISPUTES_ADMIN", "DISPUTES_USER"],
  admin: ["ADMIN", "DISPUTES_ADMIN"],
},

// server/auth/products.ts: server-only procedure builders and page guards
export const disputesProcedure = createProductProcedure({
  product: "disputes",
  tier: "viewer",
});

Asking for a tier a product does not define throws when the module loads

Authorization policy lives in those two files. They were one file until the client navigation imported the role table and pulled server environment variables into the browser bundle, which failed at runtime. The split keeps the role data importable anywhere and marks the procedure builders server-only, so the same mistake now fails at build time.

The sidebar, the mobile navigation and the home grid all derive from one navigation catalog gated by the same role check. A product a user cannot access shows as a locked tile, and navigation cannot drift from permissions. A single guard module owns every direct connection to the production database. Connecting requires one explicit flag; writing requires a second flag plus a confirmation for that specific operation.

Shared views and the products on the shell

A shared UI package holds the component primitives. A shared views package gives every product the same board, table and list layouts, with filters, sorting and view state serialized into the URL. Conventions are checked by a linter: every API router exports a router named after its file, every workspace package carries the same tooling files, and every UI primitive exports a component named after the file.

The products on the shell:

  • Support operations, the largest surface: tickets, orders, ingestion and enrichment health, cancellations and metrics for the customer-support AI agent.
  • Disputes tracks chargebacks across payment providers and brands, backed by a Go service.
  • Media buying rebuilds a spreadsheet-and-script workflow for paid-social campaigns: the rules that route ads, mix them into ad sets and name campaigns became a pure engine over typed rule objects, tested against an end-to-end golden scenario that reproduces the legacy output, with spend, sales and CPA read from the warehouse.
  • Assistant analytics: conversations, error tracking and analysis for the in-app AI assistant.
  • Video library search over marketing footage by embeddings, captions and metadata.

A shared kanban board also started in this repository and was later extracted into its own. The customer-support agent outgrew the shell too and moved into its own repositories.

Go services on a one-way kernel

The Go services share a small kernel for bootstrapping, configuration, HTTP, logging, migrations, connection pooling, observability and health probes. A test parses every Go file in the kernel tree and fails if any of them imports a service, so dependencies only point downward.

A service manifest declares each service's owner, local verification command, image, deployment mode, and health and version checks, and a script validates it. Each service reports its deployed commit at a version endpoint. That endpoint is the only accepted answer to "what is running in production"; a merged fix is not yet a deployed fix.

Evidence PDFs within an upload limit

The disputes service renders evidence documents to PDF with headless Chrome and exports business gauges such as resolution rate to Prometheus. Evidence includes customer attachments, and CSS max-width only changes how large an image is drawn. One phone photo became 21.1 MB of a 22.5 MB PDF rendered from a 16 KB HTML file. The payment provider accepts uploads up to 5 MB, and a ticket can carry several attachments, so a per-image cap bounds nothing. The service now fetches each image, resamples it, re-encodes it as JPEG and inlines it, with a size budget across the whole document and a 1,600 px ceiling on any single image.

Utility services in Go (HTML-to-PDF, PDF merging, audio conversion) serve n8n workflows behind bearer tokens. They were built to replace an older Python service, and the cutover runbook keeps the Python endpoints running until the Go path has run cleanly for two business days, so a rollback only means pointing the workflows back.

Built by agents, gated like people

The repository is written to be developed by AI coding agents. Agents work on agent/* branches, commit tests with the change, open a merge request, and enable merge only after the pipeline is green. The pipeline runs the offline gates: lint, typecheck, unit tests, manifest validation and the Go tests. Integration and performance suites need a disposable database and run locally, so a green pipeline is documented as proof of the offline gates only.

The support console's local stack starts with one command and needs no credentials and no network. Stubs stand in for external APIs and keep their state. The seed corpus is synthetic and deterministic: its shape comes from read-only production aggregates (cardinalities, enum frequencies, null rates, text-length percentiles), and every name, address and message body is written for the fixture. A coverage file declares which console states the corpus must reach, and a test fails by name when one becomes unreachable.

Agents kept writing one-off shell loops to wait for pipelines, and the loops broke: one scraped CLI text output that came back with empty fields. They were replaced by durable waiter scripts that parse structured output, tolerate transient CLI failures and exit with distinct codes. Their README also records silent CI faults, such as one push of seven branches creating pipelines for only four of them.

About 1,900 merges came from agent branches, most of them within one month. Production safety is a written contract that binds humans and automation equally: data changes, schema changes and production writes need explicit human approval for the exact target and operation, and a deploy happens only when a person runs it from a trusted machine.

Limitations

  • A shared repository couples every product's release process; the largest product eventually needed its own repositories.
  • Two policy files are easy to audit and still a hotspot for merge conflicts when many agents work at once.
  • Offline gates in CI are fast, but integration and performance suites depend on local discipline.
  • Deterministic synthetic data covers the declared states only; real production traffic still finds cases nobody declared.
  • The platform's success metrics (support and dispute resolution rates, time to ship a new product, shell adoption) are defined; I have no published measurement of them to show.
  • High agent throughput moves the bottleneck to review, deployment and product judgment, which stay human.

Related case studies

Start with one process

A one-hour call costs €80. Afterwards you get a written plan, whether or not we work together.