I’m Tired of Not Understanding What’s Going On Inside Vibe-Coded Projects

· Medium ·

10 min read Original article ↗

Igor Golovko

I handed an agent four repositories and saw what is simply invisible in the code.

Press enter or click to view image in full size

The AI coding hype has reached everyone. If you don’t use AI tools at work and in life, your skills are going stale with every week and every new model release. And this same hype has flooded the internet with crowds of SaaS products whose internals are scary to look under the hood of.

And the issue isn’t low qualification — these teams often have a perfectly decent technical background. The issue is speed: the pace of releases and decisions drags along tons of half-finished work, because in this race you have to grab your slice of the pie in time.

The concept doesn’t change: first quickly cobble together an MVP from whatever is available, ship it, get feedback. Get through the most important stage — from nothing to something tangible — as fast as possible. And only if the project takes off, rewrite it properly: the right technologies, the right approaches and patterns.

Solution architects will throw sticks at me now :)

And who is going to do all that rewriting? Who will have the context of a giant vibe-coded project? People, of course. Who will also use AI — but with an understanding of the context and existing problems.

The reverse-engineering of everything vibe-coded begins: documentation, data-flow descriptions, lots of different diagrams — so everything can be designed the way it needs to be.

And the clearer these diagrams, composition and structure are, the easier and faster it is to rewrite everything onto the right rails.

Let’s imagine: we have a SaaS vibe-coded in a couple of hours that turns podcasts and videos into sets of clips for social media.

We built it, shipped it — and the project really took off. The first users started pouring in, the hype began. And then something else starts: a mountain of bugs, user complaints, resource utilization problems, everything falling apart for users. We rush to fix bugs with agents — new bugs appear. And so it circles.

And neither the agents nor us — the technical people operating those agents — have enough context of the whole system.

Next, I tested this on a live project: I assembled such a SaaS from four repositories, handed it to an agent to reconstruct the architecture, and then rewrote the core from Node to Java by the book — with layers, types, JPA and DTOs. Spoiler: the code became several times larger and objectively better, but all five architectural problems stayed in place. One of them even became explicitly baked into the code during the rewrite. I could only see it on a diagram.

The test subject: ClipCast

To not just wave my hands, I actually built such a project. Meet ClipCast: a SaaS that takes a podcast or video and cuts it into clips for social media.

Four separate repositories, exactly as it usually looks:

  • clipcast-web — web studio: login, workspaces, projects, clips, comments. React + TypeScript (Vite)
  • clipcast-api — core: auth, workspaces, projects, media upload, API tokens. Java 21 + Spring Boot
  • clipcast-transcriber — worker: media → transcript. Python
  • clipcast-clipper — worker: transcript → clips, external AI calls. Node.js

Plus Postgres and Redis, everything starts with one command:

docker compose up --build

Press enter or click to view image in full size

Project tree: four repo folders + docker-compose.yml.

Open any of the four repositories on its own — and everything looks fine.

  • Open clipcast-api — great Spring Boot, by the canon.
  • Open clipcast-transcriber — a small tidy 50-line Python worker.
  • Open clipcast-clipper — an equally small Node worker.
  • Open clipcast-web — typed React.

But here are questions that none of these repositories answers:

  • Who actually writes to the clips table? (Spoiler: not what you think.)
  • How many services hold a connection to the same database?
  • What happens if the transcripts schema changes?
  • Which API endpoints are unprotected by authorization?
  • Which endpoints does nobody call at all?

Keeping this in your head across four repositories is still possible. Across twelve — not. And the agent fixing the next bug doesn’t get this context at all: it sees one repository and its prompt.

Now — exactly the experiment this was all for.

Step 1. Get a token in Viaduct

Viaduct is a completely free tool where I keep the architecture: a C4 model (systems → containers → components), HTTP contracts, broker channels, ER schemas, PlantUML sequences, and Magic flows (replayable data flows across the whole system).

Key for this article: it has an MCP server. So the agent can not just “look at a picture” — it can read and write the model with tools.

Create an API token:

Press enter or click to view image in full size

Path: gear icon and MCP Access

Create the token, copy the agent config with it, and ask the agent to set up its MCP connection.

Step 2. Connect the MCP server to the agent

I work in Claude Code, so the command is:

claude mcp add - transport http viaduct https://c4.quietgridlabs.com/api/mcp \
- header "Authorization: Bearer $VIADUCT_TOKEN"

Check that the agent actually reached it — the simplest call:

c4_whoami

If your user comes back in the response — the agent is connected to the architecture.

Press enter or click to view image in full size

The agent calls tool c4_whoami or any other, and Viaduct sees the connection succeeded.

The agent calls tool c4_whoami or any other, and Viaduct sees the connection succeeded.

Press enter or click to view image in full size

Agent connected successfully

Then run:

claude mcp list

Press enter or click to view image in full size

To make sure the MCP server is connected successfully

Press enter or click to view image in full size

MCP Viaduct successful connection screen

MCP Viaduct successful connection screen

The full toolset the agent gets is roughly what a living architect uses:

  • reading: c4_project_context, c4_search, c4_get_element, c4_list_docs, c4_list_technologies
  • model writing: c4_create_element, c4_update_element, c4_upsert_connection
  • docs and flows: c4_upsert_doc, c4_upsert_sequence, c4_upsert_data_flow

One small detail that turned out important: Viaduct has a skill (viaduct-architect) — a set of rules for how exactly to model. That an endpoint is kind=endpoint with a method and a contract, and a topic is kind=channel under a broker, not "yet another endpoint". That connections only exist between elements of the same C4 level. That technology is an id from the catalog, not the string "Redis".

Without these rules the agent draws a mess of “Service A talks to Service B”. With them — you get a model you won’t be ashamed to show the team.

Step 3. Ask the agent to document the project

Now the interesting part. The prompt is essentially one:

Scan the code in ~/clipcast-demo (4 repositories: clipcast-web, clipcast-api,
clipcast-transcriber, clipcast-clipper) and document the architecture in Viaduct:
create a system for each service, endpoints, broker channels, connections between
services — including hidden ones. Mark the hidden coupling separately; it must be
visible on the diagram.

And the agent goes to work: it reads pom.xml and controllers, worker.py and worker.js, schema.sql, docker-compose.yml, package.json — and lays it all into the model in parallel.

Press enter or click to view image in full size

Agent starts documenting

Press enter or click to view image in full size

Agent builds the model

We wait for the agent to create everything in the project.

12 minutes later, the agent had fully documented all repositories.

Press enter or click to view image in full size

Successful completion and a brief summary from the agent

What we got

Viaduct now contained:

  • 1 actor — the studio user
  • 5 systems, 1 of which is the external OpenAI API
  • 16 API endpoints — with methods, paths, request bodies and all response statuses, not just the happy path
  • 2 broker channels on Redis: media.uploaded and transcript.ready — with message schemas
  • 9 Postgres tables with columns and types
  • documentation for the system and every significant container
  • a PlantUML sequence diagram for the main scenario
  • a Magic flow “Upload → Clips pipeline” with 9 steps — the entire path of a file from upload to a finished clip

Press enter or click to view image in full size

Top level, as requested — the agent grouped them into systems

Press enter or click to view image in full size

A crowd of Postgres tables unconnected to each other

Press enter or click to view image in full size

Data flow for clip upload

Press enter or click to view image in full size

Catalog with documentation and endpoints

And now the reason this was all for

Once the model was assembled, the diagram showed what the code spreads across four repositories.

1. Three services write to the database, not one

clipcast-api seems to be the only entry point to the data. But clipcast-transcriber holds its own psycopg2 connection to the same database and does INSERT INTO transcripts. And clipcast-clipper holds its own pg pool and does INSERT INTO clips.

In the code these are two unremarkable files, db.py and db.js, five lines each. On the diagram — three arrows converging on one Postgres, two of them labeled "directly, bypassing the api".

The consequence you can’t see in the code at all: any schema migration in the Java service quietly breaks two other services in other languages. Hibernate with ddl-auto: update cheerfully changes the schema itself.

2. The pipeline is half event-driven, half HTTP

The whole chain is built on Redis pub/sub: media.uploaded → transcript → transcript.ready → clips. But the finalization clipper sends as a direct HTTP call — POST /internal/clips/ready.

This once was a quick fix. In clipper’s code it’s a single fetch line. On the diagram it's a separate arrow that breaks the symmetry of the rest of the flow — and you immediately want to remove it.

3. An internal webhook with no authorization

/internal/clips/ready checks nothing at all: neither authorization, nor that projectId belongs to the caller. Anyone who can reach the port can mark someone else's project as ready.

4. Dead code nobody dares delete

GET /api/legacy/ping — an old health check. No calling code exists in any of the four repositories. The agent tagged it dead-code.

5. Hardcoded API address in the frontend

const API_BASE = 'http://localhost:4000' — no environment variables.

The main point

None of the five items is “bad code”. Each on its own is a reasonable compromise made when you had to ship in time. The problem is that together they exist only between the repositories, and you can only see them on a map of the whole system.

What this gives you in practice

Now I have a map of the system, and it’s not in one person’s head.

  • For me as a technical specialist: I see which threads to pull. Not “we should refactor sometime” but specifically: remove two direct database connections, lock down /internal/*, replace the HTTP notification with an event. Priorities became visible.
  • For agents: this is exactly the missing context. When I give an agent the task “add an endpoint for downloading a clip”, it can first read the model and learn that the clips table is written by a completely different service in a different language. Without this, it would simply add an INSERT to the Java service and create a third writer to the same table.
  • For new people on the project: onboarding is opening a diagram and replaying a Magic flow, not reading four repositories in a row.

A prompting tip that actually affects the result: don’t write “document the project”. Ask to document specific things — endpoints with contracts, channels, database connections — and separately ask to highlight whatever looks suspicious. The difference between “drew boxes” and “found three arrows into one database” is exactly this phrase in the prompt.

Extraordinary times require extraordinary solutions. We’re not going to stop vibe-coding — and we shouldn’t. But if code is generated ten times faster than before, the map of that code has to be updated ten times faster too. By hand, that’s no longer possible.

Links: