Sixteen Days of Software

A luminous travel timeline across Madagascar

WebSocket sync in real time, PostgreSQL underneath, custom sign-in on top. Three travelers. Sixteen days. Then it’s done.

This is the fifth and final experiment. Over five articles, the gap between “describe” and “works” got smaller. Not because the tools got better between articles, but because I did. A year of building dozens of these apps taught me what to ask for, how to structure a prompt, when to push back, when to let the tool run. TriboTrip is what vibe coding looks like when you stop experimenting and start building.

The idea

A family trip to Madagascar. Home, or the version of it that thirty years abroad leaves intact. Sixteen days, three travelers, six flights, four destinations. Dublin to Paris to Antananarivo to Sainte Marie and back. Ferries, internal flights, hotels, restaurants, things to do. The kind of itinerary that usually lives across three email threads, a shared Google Doc nobody updates, and someone’s WhatsApp messages.

The name: Tribe + Trip. A trip planner for the tribe.

I didn’t prompt “build me a trip planner.” Dozens of projects had taught me that every requirement you leave implicit is a decision the tool makes for you, usually wrong. So I specified: six entry types (flights, accommodation, transportation, activities, dining, sightseeing). Real-time sync so family members see changes instantly. Custom auth so each family member has their own login. Multi-day spanning entries for hotels. Timezone handling between Europe and Madagascar. File uploads for booking confirmations.

That specificity is what changes with experience. You learn to speak the tool’s language. Not code, not English, something in between: a set of constraints shaped by everything you know about where software goes wrong. Every sentence you add narrows the space where the tool can make a wrong decision.

Look at what those sentences actually do. “Six entry types” tells the tool what kinds of things exist in the app before a single line of code exists. Without it, you get a generic text field and spend three days reshaping the app. “Real-time sync” tells the tool this can’t be an app where everyone keeps refreshing the page; those two words push it toward WebSockets instead of polling. “Timezone handling between Europe and Madagascar” and “multi-day spanning entries” are the tricky cases, stated up front, which is why the two hardest bugs never arrived.

And notice what the prompt doesn’t say. No mention of React, Tailwind, or PostgreSQL. No database choice, no component library, no preferred app structure. Specifying technology is over-constraining; the tool picks a coherent stack on its own and that’s fine.

Specifying behavior is the leverage. The early prompts in this series specified features: build me an interactive periodic table, give me a quiz mode. The mature prompts specify boundaries: here is where the data is complex, here is where users will collide, here is where the math gets tricky.

What I got

Forty-three commits over three days. React, TypeScript, Tailwind, shadcn in the browser. Express, PostgreSQL, Drizzle ORM, WebSocket on the server. In practice that meant a full app with a timeline organized by day, color-coded entry types, live collaboration, location lookup, file uploads, and custom sign-in.

Timeline view showing Day 1 flights from Dublin to Antananarivo with layover details and hotel check-in

Seventy lines of real-time

Six entry types, each with shared fields and its own specifics, plus multi-day stays with check-in and check-out visuals. The Drizzle schema alone runs to a hundred lines. But the real story is the live-update system. The whole WebSocket layer is compact. On the server, a single broadcast function:

function broadcast(wss: WebSocketServer, data: any) {
  const message = JSON.stringify(data);
  wss.clients.forEach((client) => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  });
}

// Every CRUD operation follows the same pattern:
app.post("/api/trip-entries", isAuthenticated, async (req, res) => {
  const data = insertTripEntrySchema.parse(req.body);
  const entry = await storage.createTripEntry(data);
  broadcast(wss, { type: "entry_created", data: entry });
  res.status(201).json(entry);
});

On the client, a single hook listens for those broadcasts and marks the right cached data as stale:

export function useWebSocket() {
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();

  const connect = useCallback(() => {
    const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
    const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      switch (message.type) {
        case "entry_created":
        case "entry_updated":
        case "entry_deleted":
          queryClient.invalidateQueries({ queryKey: ["/api/trip-entries"] });
          break;
        case "member_created":
        case "member_updated":
          queryClient.invalidateQueries({ queryKey: ["/api/trip-members"] });
          break;
        // ... comments, photos, day locations
      }
    };

    ws.onclose = () => {
      reconnectTimeoutRef.current = setTimeout(connect, 3000);
    };
    wsRef.current = ws;
  }, []);

  useEffect(() => {
    connect();
    return () => {
      if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
      wsRef.current?.close();
    };
  }, [connect]);
}

When one family member adds a flight, the server broadcasts the event. Every other open browser receives the message, marks its local copy as out of date, and fetches the fresh version on the next render. Real-time collaboration. No manual refresh. The whole system in about seventy lines across two files.

This is not a toy. This is a full application with live collaboration, a real database, proper sign-in, and file uploads.

But the compactness has a cost. The broadcast layer sends every event to every connected client, and it does so as data: any: basically a generic payload with no channels and no filtering. For three family members that’s fine. For thirty it would be the first thing to rebuild.

What I had to fix

Not much. That’s the story.

The auth system needed a pivot. Replit’s built-in authentication wasn’t right for the use case: I wanted family members to log in with a simple username and password, not through a Replit account. A few commits swapped it out for custom auth with hashed passwords. Straightforward.

Login screen showing trip overview — 16 days, 4 destinations, 6 flights, 3 travelers — with simple username and password auth

Transport duration calculations took more work. Six or seven commits refining edge cases around multi-day journeys and timezone math. When a flight departs Dublin at 10:00 GMT and arrives in Antananarivo at 22:00 GMT+3, the display needs to account for both the timezone offset and the date change. The kind of thing that’s easy to describe and fiddly to get right.

Two issues. A handful of commits. Compare that to the NATO Alphabet Coach’s nine-day debugging saga or the Periodic Table’s iterative grid adjustments. Experience doesn’t make the tools smarter. It makes the prompts better, and better prompts mean fewer surprises.

The gap

There is a gap between what people assume an AI-generated app looks like and what the tools actually produce. It is wider than most people think.

Settings showing six color-coded entry types with customizable colors in dark mode

Architecture was never the hard part. The tool can produce the plumbing for live updates and data storage without breaking a sweat. The hard part is knowing what kind of architecture to ask for. At some point during the build, I stopped thinking about Replit, about prompting strategy, about what the AI could or couldn’t do. I was deciding whether the ferry to Sainte Marie needed its own entry type or fit under transportation. Whether timezone display should show the offset or the city name. Product decisions, not engineering ones.

Vibe coding made the engineering cheap. The decisions stayed expensive. The code runs. I haven’t audited every dependency. For sixteen days of family travel, I don’t need to.

The trip planning became the story. That’s the real transition. Not from bad tools to good tools. From thinking about the tool to thinking about the problem.

The lesson

A drill app for one person’s NATO alphabet practice. A periodic table for one chemistry student. A trip planner for one family’s two-week holiday. Under the old economics, none of these justified building. No market, no team, no business case. They would have stayed ideas.

They exist because building them costs a weekend.

That change happened fast and most of the industry hasn’t noticed.

That barrier is lower. Not gone: these tools cost money, they need reliable connectivity, and “a free weekend” is itself a resource not everyone has.

For a developer in Nairobi or Antananarivo, the same tools are less likely to be a weekend curiosity and more likely to be competitive leverage, a way to ship for clients at a speed that used to require a team. Not building for an audience of one. Building for income, for a community that needs software shaped to local problems.

This series describes the hobby version of that shift, from a position secure enough to explore. But positions change. I grew up in Antananarivo. I left as a child, built a career through France and then Dublin, and never stopped being connected to home. The developer still there isn’t building for an audience of one. They’re building for the problems right in front of them, with an understanding of those problems that no amount of prompting from Dublin can replace.

Some of what starts personal turns out not to be. The space between “too small to build” and “big enough to sell” used to be empty. That’s where the productypes live, and it isn’t empty anymore.

A fleet of one

TriboTrip will be used for sixteen days by a family. Then it’s done. Not abandoned, not deprecated. Complete. Software that did what it was made for, when it was needed, for the people who needed it.

My day job builds software with teams of people. These five experiments were a team of one and an army of agents. The way a single engineer now manages a fleet of thousands of machines, a single developer can build and sustain a fleet of apps. I haven’t found the ceiling yet.

What remains

A year ago I worried that these tools would make me obsolete. That fear was too simple. What they actually did was make me into a different kind of builder, one I’m still getting to know.

I felt something like this once before. The first time I used ReSharper in Visual Studio, typing stopped being typing. Rename a symbol, select a destination, and the codebase reorganized itself through a trance of keystrokes. Martin Thompson calls this mechanical sympathy: understanding the machine well enough to move through it instead of against it.

What’s forming now is not quite that. The machine is different; ask it the same question twice and the code comes back structured differently. You can’t build sympathy for something that reshuffles on every pass. But you can build sympathy for the problem. Thirty years of building gave me a feel for what a solution should look like, the way a carpenter can tell from the sound whether a joint is tight. The sympathy moved from the machine to the material.

But here is what I can’t answer yet. The feel I have for the problem comes from thirty years of trial and error. The tools are accumulating their own. Each new model gets better at guessing what I left out of the prompt. The distance between “I know what to ask for” and “the tool already knows” keeps shrinking. If the carpenter’s ear is the last advantage, what happens when the machine stops producing loose joints?

The vertigo hasn’t gone. It just stopped being the whole view.