Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,45 @@ Built for the *Built with Opus 4.7* Claude Code hackathon (April 21–26, 2026).

---

## The Meridian scenario

Meet the problem Convoy was built to solve.

Meridian is a three-service fintech startup: an orders API (Express + Postgres), a PDF report renderer, and a payments worker. Their principal engineer Karan is leaving next week. Production is throwing intermittent 503s on the orders service — a mystery bug that only Karan fully understands. The CTO wants to ship a payment-processing refactor and bring up the new reports service before Karan walks out the door.

Without Convoy, the team is blocked. Every deployment went through Karan: he knew which environment variables were missing on Fly, which services depended on which, what the rollback story was. None of that is written down. The new engineer is afraid to touch prod.

With Convoy, they run one command:

```bash
convoy plan ./meridian-orders --save
convoy apply <plan-id>
```

Here's what happens in the next 8 minutes:

1. **Scan** — Convoy reads the repo. It finds the Express app, the Prisma schema, the BullMQ worker, the `.env.example` with `DATABASE_URL` and `STRIPE_SECRET_KEY`. It detects the health endpoint at `/health`. No config, no annotation — it reads the evidence.

2. **Pick** — Fly.io scores highest (+25 for the worker topology, +15 for the Dockerfile). The platform decision is inspectable: a score table with the delta for each signal, so the team can disagree and override with `--platform=railway` if they want.

3. **Rehearse** — Before a single line of config is committed, Convoy spawns the orders service locally, hits it with 60 synthetic requests, and scrapes the metrics. This is where it catches the bug: p99 of 8,740ms. Zero errors — all requests succeeded — but every user waited 8 seconds. The medic agent (Claude Opus) reads the logs and reports: _"I found a global `renderLock` in `src/routes/render.ts` that serialises all PDF requests through a single promise chain. Replace it with a semaphore."_ The pipeline stops. Nobody approved anything. Nobody got paged at 2am.

4. **Fix and resume** — the new engineer fixes the lock. They run `convoy resume`. Convoy carries the uncommitted fix onto the convoy branch as a `fix:` commit — the medic's diagnosis is the commit subject. Rehearsal passes: p99 142ms.

5. **Author gate** — Convoy pauses. The approval card shows rehearsal evidence: p99, error rate, smoke tests. The CTO approves from evidence. Convoy opens the PR with the Dockerfile, `fly.toml`, CI workflow, and `.env.schema`.

6. **Secrets gate** — Before the deploy command runs, Convoy diffs the expected vars against what Fly has staged. `STRIPE_SECRET_KEY` is missing. It pauses — not fails, pauses — surfaces the key in the web UI, and lets the operator paste the value inline. Convoy pushes it to Fly via `fly secrets set` and proceeds.

7. **Canary** — Convoy deploys to one machine at 5% traffic, compares the p99 delta (+3ms), declares healthy, and promotes.

8. **Observe** — 120 seconds of post-deploy monitoring. SLO healthy. Done.

Total time: 8 minutes. Total tribal knowledge required: zero. The plan page is the handoff document: what was scanned, what was scored, what rehearsal found, what secrets were staged.

Karan can leave. The team can ship.

---

## See it in action

[![Convoy: AI Deployment Agent from Commit to Production](https://img.youtube.com/vi/5btzce8adeE/maxresdefault.jpg)](https://www.youtube.com/watch?v=5btzce8adeE)
Expand Down
7 changes: 7 additions & 0 deletions demo-app/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
PORT=8080

# DEMO_MODE controls which failure scenario is active:
# stable — normal behaviour, all routes fast and healthy (default)
# buggy — every 10th GET /orders returns 500 with high latency
# simulates a stuck downstream dependency; triggers error-rate breach
# concurrent — POST /render serialises through a global lock (single-threaded renderer)
# simulates a thread-safety bottleneck; triggers p99 breach under load
DEMO_MODE=stable
86 changes: 86 additions & 0 deletions demo-app/src/routes/render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Router, type Request, type Response } from 'express';

import { log } from '../lib/logger.js';

const DEMO_MODE = process.env['DEMO_MODE'] ?? 'stable';

const router = Router();

// Global serialisation lock. Only one report renders at a time because the
// underlying PDF library is not thread-safe. This was fine at 1-2 concurrent
// users; at 30 it means the last request waits 30 × RENDER_MS before it even
// starts.
//
// TODO: replace with a bounded worker pool (limit=4) — this single lock
// serialises all render requests through one queue.
let renderLock: Promise<void> = Promise.resolve();

const RENDER_MS = 300; // realistic single-page PDF render time

router.post('/render', async (req: Request, res: Response) => {
const { reportId = 'unknown', pages = 1 } = (req.body ?? {}) as {
reportId?: string;
pages?: number;
};

const queued = Date.now();

if (DEMO_MODE === 'concurrent') {
// Acquire the global lock — block until the previous render finishes.
const prev = renderLock;
let release!: () => void;
renderLock = new Promise<void>((resolve) => {
release = resolve;
});

const waitMs = await prev.then(() => Date.now() - queued);
log({
level: 'info',
message: 'render_lock_acquired',
reportId,
waited_ms: waitMs,
});

const renderStart = Date.now();
await sleep(RENDER_MS * Number(pages));
const renderMs = Date.now() - renderStart;

release();

const totalMs = Date.now() - queued;
log({
level: 'info',
message: 'render_complete',
reportId,
pages,
render_ms: renderMs,
total_ms: totalMs,
});

res.status(200).json({
reportId,
pages,
render_ms: renderMs,
total_ms: totalMs,
url: `/reports/${reportId}.pdf`,
});
} else {
// Stable mode: renders are independent and fast.
await sleep(20 + Math.random() * 20);
const duration = Date.now() - queued;
log({ level: 'info', message: 'render_complete', reportId, pages, total_ms: duration });
res.status(200).json({
reportId,
pages,
render_ms: duration,
total_ms: duration,
url: `/reports/${reportId}.pdf`,
});
}
});

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

export default router;
2 changes: 2 additions & 0 deletions demo-app/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { metrics } from './lib/metrics.js';
import healthRouter from './routes/health.js';
import metricsRouter from './routes/metrics.js';
import ordersRouter from './routes/orders.js';
import renderRouter from './routes/render.js';

const PORT = Number(process.env['PORT'] ?? 8080);
const DEMO_MODE = process.env['DEMO_MODE'] ?? 'stable';
Expand All @@ -25,6 +26,7 @@ app.use((req, res, next) => {
app.use(healthRouter);
app.use(metricsRouter);
app.use(ordersRouter);
app.use(renderRouter);

app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
log({ level: 'error', message: 'unhandled_error', error: err.message, stack: err.stack });
Expand Down
153 changes: 153 additions & 0 deletions docs/demo-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Convoy Demo Script

## Context

This script uses the **Meridian** scenario: a three-service fintech startup. Their principal engineer (Karan) is leaving next week. Production is throwing intermittent 503s on the orders service. The CTO wants to ship a payment-processing refactor and bring on a new service (reports/PDF renderer) — all before Karan walks out the door.

The audience is a technical founder, lead engineer, or DevOps-aware developer who has felt the pain of deployment becoming a team-wide bottleneck.

---

## 3-Minute Pitch

> _Speak this. Eye contact. No live demos yet._

"Let me paint a picture you've probably lived.

Your best engineer is context for how everything deploys. Not because they hoard knowledge — because deployments accumulated sharp edges and only they know which ones bite. New engineers are afraid to touch prod. The ones who aren't afraid break it.

You want to ship a payments refactor. Three questions you need answered before you can confidently merge it: Will it break the orders service? Are the new Stripe keys staged on Fly? If it does go sideways, how fast can you revert? Those answers are either in someone's head or in Slack history from 2022.

Convoy turns those questions into evidence that travels with every deployment.

**What it does:**
- Scans your repo — language, framework, health endpoint, Postgres, Stripe keys you're going to need
- Picks the right platform — fly, vercel, railway, cloud run, or your own VPS — with scored reasoning you can inspect
- Rehearses locally before writing a single line of config — synthetically loads the service, watches error rate and p99
- Authors exactly the files Fly/Vercel needs — Dockerfile, fly.toml, CI workflow, .env.schema
- Secrets gate before the deploy — diff against what's on the platform, pause if anything's missing
- Canary at 5%, observe for 2 minutes, promote or roll back automatically

Every forward action has a pre-staged reverse. The medic agent — Claude Opus — reads the logs and tells you the root cause in first-person: 'I found a global render lock in src/routes/render.ts that serialises all PDF requests through a single promise chain.'

The Meridian team ships Karan's work. Without Karan on the call."

---

## 10-Minute Live Walkthrough

### Setup (before the demo — do not narrate)

```bash
npm install && (cd web && npm install) && (cd demo-app && npm install)
cp .env.example .env # add ANTHROPIC_API_KEY
(cd web && npm run dev) & # http://localhost:3737
```

---

### Act 1 — Plan (2 min)

**Narrate:** "I'm going to point Convoy at the Meridian orders service — this is the Node/Express backend, the one throwing 503s."

```bash
npm run convoy -- plan ./demo-app --save
```

While it runs:
> "Watch the scanner. It's reading the package.json, finding the Express routes, detecting Prisma, inferring the health endpoint. No config — it reads the evidence."

After it finishes, open the plan URL from CLI output — or:
```bash
PLAN_ID=$(npm run convoy --silent -- plans | head -1 | awk '{print $1}')
open http://localhost:3737/plans/$PLAN_ID
```

**In the web UI, point out:**
- **"Why this platform"** section — Fly scored highest. Show the +25 for background-worker topology, +15 for container-native. "This isn't magic: it read that there's a BullMQ worker and a Dockerfile."
- **"What Convoy will author"** — expand the Dockerfile. "It detected Node 22, pnpm, the Prisma schema. The Dockerfile knows to run `npx prisma generate` before the build."
- **"Required secrets"** on the lane card — `DATABASE_URL`, `STRIPE_SECRET_KEY`. Amber dot — not staged yet. "The scanner read `.env.example`. It knows what prod needs before I do."

---

### Act 2 — Rehearse the Concurrency Bug (3 min)

**Narrate:** "Before we write any config files, Convoy rehearses. Let me turn on the concurrency failure mode — this is the PDF renderer bug Karan found last week."

```bash
npm run convoy -- apply $PLAN_ID --inject-failure=concurrency
```

> "Convoy is starting the service in concurrency mode — 30 simultaneous /render requests. Watch the p99."

After the breach:
> "p99 of 8,740ms. Zero errors — all requests succeeded. That's the deceptive part. Your error rate looks fine but every user waited 8 seconds for their report. The medic is reading the logs now."

When the medic card appears in the web UI, show it:
> "Opus read the logs and traced the `render_lock_acquired` entries. It found `renderLock` in `src/routes/render.ts` — a global `let renderLock: Promise<void>` that serialises every render through a single promise chain. It suggests replacing it with a semaphore."

**Key message:** _Convoy caught this in rehearsal, not in production. No customer saw an 8-second report load._

---

### Act 3 — Clean Run (2 min)

**Narrate:** "Let me fix the lock and rerun — but in scripted mode so we don't need real credentials for this demo."

```bash
npm run convoy -- apply $PLAN_ID
```

> "Rehearsal passes — p99 of 142ms, 8/8 smoke tests. Now Convoy pauses at the PR gate."

When the `open_pr` approval appears in the web UI:
> "The operator sees rehearsal evidence before approving. p99, error rate, smoke tests. You're approving from evidence, not from 'Karan says it's fine.'"

Click Approve in the web UI. Continue to canary:
> "Scripted canary — 5% traffic, baseline comparison shows +3ms p99 delta. Within tolerance. Convoy promotes."

---

### Act 4 — Secrets Gate (1 min)

**Narrate:** "Let me show what happens when `STRIPE_SECRET_KEY` is missing on Fly."

```bash
npm run convoy -- apply $PLAN_ID --real-fly --fly-app=meridian-orders
```

> "Before the deploy command runs, Convoy diffs the expected keys against what Fly has staged. `STRIPE_SECRET_KEY` is missing. It pauses — not fails, pauses — and shows you the gate in the web UI."

In the web UI, point to the `stage_secrets` approval card:
> "The form lets you paste the value right here. Convoy pushes it to Fly via `fly secrets set` and then continues the deploy. The pipeline never stalled — it paused, collected what it needed, and proceeded."

---

### Act 5 — The Handoff (2 min)

**Narrate:** "Here's the part that matters for the Karan scenario."

```bash
npm run convoy -- plans
```

> "This plan is a record. What was scanned, what was scored, what rehearsal found, what was staged. Karan's replacement opens this page, sees the Dockerfile preview, sees that `DATABASE_URL` is already marked staged, sees the p99 baseline from the last rehearsal."

Back to the web UI plan page:
> "They don't need Karan's tribal knowledge. They need this page and a `convoy apply`."

---

## Objection Handling

**"We already have CI/CD."**
> "CI runs on the commit you already pushed. Convoy runs before the PR — it rehearses your code on your machine before a single file is committed. The CI/CD pipeline and Convoy are complementary: Convoy validates before the PR, CI validates the PR content."

**"We have Terraform / Pulumi for infra."**
> "Convoy doesn't touch your Terraform. It authors the application layer — Dockerfile, fly.toml, the CI workflow that invokes your existing infra. If you have a VPS with docker-compose, it goes there instead."

**"We'd have to trust Convoy to not break anything."**
> "Convoy never authors your application code. It authors exactly five artifact types: Dockerfile, platform manifest, CI workflow, .env.schema, infra/ terraform stubs. Every forward action has a pre-staged reverse. The medic writes diagnoses; it doesn't write fixes."

**"What if Convoy's platform choice is wrong?"**
> "Override it. `--platform=vps` or `--platform=railway`. The score table still shows why each platform ranked where it did — you can audit it, disagree, and ship to wherever you want."
Loading
Loading