# Build an AI summarizer with background jobs: retries, signed webhooks and Postgres over one MCP connection

> Source: <https://dev.to/cloudhike/build-an-ai-summarizer-with-background-jobs-retries-signed-webhooks-and-postgres-over-one-mcp-1c3k>
> Published: 2026-11-02 19:42:04+00:00

*This is week 2 of **Build with CloudHike**, a weekly series where we build a small, real-world app with an AI coding agent and let the agent set up the backend. Disclosure: I built [CloudHike](https://cloudhike.dev?utm_source=devto&utm_medium=article&utm_campaign=build_series_w2), so keep that in mind.*

Every AI feature starts the same way: one `await` on a model call inside an API route. It works on your laptop. Then real users paste long documents, the model takes its time, the provider answers 429 now and then, and your request sits there waiting while the browser spinner keeps spinning. A failed call means the user clicks again, and you pay for the same work twice.

The usual fix is a job queue: a queue service, a worker process somewhere to run it, retry logic, and a way to get the result back to your app. That's a small piece of infrastructure before you've improved anything for the user.

This week we'll build an **AI summarizer** that moves the slow model call into a **CloudHike background job**, with retries, a signed webhook for the result, and duplicate protection. As always, the agent sets up the backend over **one MCP connection**, and every prompt is below.

A "paste text, get a summary" app:

`queued` and starts a background job run, so the user never waits on the model.`x-cloudhike-signature` header, skips run ids it has already handled, and saves the summary.
Here's the flow:

`POST /api/summaries` → a row in Postgres plus a new run, answered right away`POST /api/jobs/summarize` (your worker) → AI model → summary as JSON`POST /api/jobs-webhook` → signature check → duplicate check → summary saved`GET /api/summaries/:id` until the summary is there
And here's how each piece maps to CloudHike, all in one project:

`DATABASE_URL`)` JOBS_API_URL`, `JOBS_API_KEY`)` AI_BASE_URL`, `AI_API_KEY`)
Normally that's a database host, a queue, a worker host and an AI provider account: four places to sign up and four sets of keys. Here it's one project, one `.env` and one credit balance. You don't need an OpenAI, Anthropic or Google account; you pick a model by name, such as `openai/gpt-5-mini` or `anthropic/claude-sonnet-4.5`.

If you did week 1, you're already connected; skip to Step 2.

In the CloudHike dashboard, open **Connect MCP** in the sidebar, pick your editor and click **Copy with key**. That creates your workspace API key and gives you a snippet with the key already filled in. The server is remote (Streamable HTTP), so there's nothing to install.

**Claude Code**

```
claude mcp add --transport http --scope user cloudhike https://cloudhike.dev/api/mcp \
  --header "Authorization: Bearer icu_your_api_key"
```

Run `/mcp` inside Claude Code to check that it's connected.

**Cursor** (`~/.cursor/mcp.json`)

```
{
  "mcpServers": {
    "cloudhike": {
      "url": "https://cloudhike.dev/api/mcp",
      "headers": {
        "Authorization": "Bearer icu_your_api_key"
      }
    }
  }
}
```

Open Cursor Settings → MCP and look for the green dot. Snippets for the other editors are in the [MCP docs](https://cloudhike.dev/docs/mcp?utm_source=devto&utm_medium=article&utm_campaign=build_series_w2).

Treat that key like a password. It lives in your home folder, not in the repo, and you can revoke it from the same page.

```
npx create-next-app@latest summarizer
cd summarizer
```

Open the folder in Cursor, or run `claude` in it.

Paste this prompt:

Use the cloudhike MCP tools to set up the backend for this app. It's an AI summarizer that runs the model call in a background job.

Create a CloudHike project called "summarizer" and deploy postgres and jobs. Wait until they're ready. Put the .env into .env.local, make sure it's git-ignored, and don't show me any secret values. Then read the jobs, postgres and ai docs with get_docs so you know how to use them. Don't write app code yet. Just tell me which services are running and which variable names are in .env.local.

Behind the scenes, the agent works through CloudHike's MCP tools:

`create_project` makes the project (or `list_projects` finds it if it already exists).`deploy_services` deploys Postgres and Jobs, with the same plan and billing checks as the dashboard.`wait_for_services` waits until each one reports ready.`get_environment` returns the project's `.env`, which the agent writes into `.env.local`. It can also ask for `format: "names"` to list just the variable names with no secrets.`get_docs` pulls the Jobs, Postgres and AI pages as Markdown, so the code it writes next follows the real API instead of a guess.
**One click for the AI models.** `deploy_services` covers Postgres, Auth, Storage, Redis, Jobs and Email, so the AI service is the one you deploy yourself: open the project in the dashboard and click **Deploy** on the **AI** card. Then ask:

Use the cloudhike get_environment tool again and update .env.local, keeping any keys I added. Show me only the variable names and confirm AI_BASE_URL and AI_API_KEY are there.

Read [https://cloudhike.dev/docs/md/postgres](https://cloudhike.dev/docs/md/postgres). Using node-postgres, add a migration script to this repo that creates two tables:

- summaries: id (uuid, default gen_random_uuid()), input_text, status ('queued', 'done' or 'failed'), summary (nullable), run_id (nullable, unique), error (nullable), created_at and updated_at.
- handled_runs: run_id (text primary key) and handled_at. Run the migration with DIRECT_URL if .env.local has it, otherwise DATABASE_URL. Create the app's pool once in lib/db.ts from DATABASE_URL, keep it on globalThis so hot reloads reuse it, and use max 5 connections. Then use the cloudhike describe_table tool to show me both tables.

`handled_runs` is our duplicate guard, and we'll come back to it in Step 7. The migration goes through `DIRECT_URL` because `DATABASE_URL` goes through a connection pooler (PgBouncer), and the [Postgres docs](https://cloudhike.dev/docs/postgres?utm_source=devto&utm_medium=article&utm_campaign=build_series_w2) recommend the direct address for migrations.

Same safety model as week 1: CloudHike's `query_database` tool always runs one statement in a `READ ONLY` transaction, `execute_sql` (for writes) is Pro-only and your editor asks before running it, and **no MCP tool can delete a project or service**. Schema changes live in your repo as migrations.

This is the route CloudHike Jobs will call. It holds the slow part, so nothing user-facing waits on it.

Read [https://cloudhike.dev/docs/md/jobs](https://cloudhike.dev/docs/md/jobs) and [https://cloudhike.dev/docs/md/ai](https://cloudhike.dev/docs/md/ai). Add lib/jobs-signature.ts with a verify(rawBody, header) function based on the x-cloudhike-signature example in the jobs docs: HMAC-SHA256 of "." with JOBS_API_KEY, compared with timingSafeEqual, refusing timestamps more than 300 seconds old.

Then add POST /api/jobs/summarize:

- Read the raw body with request.text() and answer 401 if verify fails.
- Parse { summaryId } and load that row's input_text from summaries.
- Call the AI service with the OpenAI SDK, baseURL AI_BASE_URL and apiKey AI_API_KEY, model "openai/gpt-5-mini", asking for a short summary in plain text.
- Answer 200 with JSON { summary }.
- If the AI call fails with a 429 or 5xx, or the network fails, answer 503 so the job retries. For any other error, answer 422 with a short message, so the run fails without retrying. Server code only. Never send JOBS_API_KEY or AI_API_KEY to the browser.

A few details from the [Jobs docs](https://cloudhike.dev/docs/jobs?utm_source=devto&utm_medium=article&utm_campaign=build_series_w2) shape this route:

`url` and to your `callbackUrl`. The `t=<unix seconds>,v1=<hex>`, where `v1` is an HMAC-SHA256 of `<t>.<raw body>` with your `retries` extra tries (0 to 5, default 3). A timeout or any other answer, such as 400, is final. So the worker decides: 503 means "try again", 422 means "don't bother".`{ summary }` is exactly what the webhook will receive.
Add POST /api/summaries. Validate that the text isn't empty and is at most 20,000 characters. Insert a summaries row with status 'queued'. Then, from server code, start a CloudHike job run: POST to JOBS_API_URL/runs with the x-api-key header set to JOBS_API_KEY and the body { url: APP_URL + "/api/jobs/summarize", method: "POST", body: { summaryId }, retries: 3, timeoutSeconds: 300, callbackUrl: APP_URL + "/api/jobs-webhook" }. APP_URL is a new env variable with the app's public address. Save the returned run id in the row's run_id and answer 202 with the summary id. If starting the run answers 429, mark the row failed with "Busy, try again in a minute".

Add GET /api/summaries/[id] that returns status, summary and error from Postgres. Then build the home page: a textarea, a Summarize button, and a result area that polls GET /api/summaries/[id] every 3 seconds until the status is done or failed.

Starting a run is one `fetch`. CloudHike answers at once with the run's `id` and status `queued`, then its worker calls your URL and waits for the answer, for up to 5 minutes on Free and 60 on Pro (`timeoutSeconds` goes up to 300 on Free and 3600 on Pro). Your API route is done in the time it takes to insert a row and make that one call.

We pass the summary id in the job `body` rather than the whole text, which keeps the job small (bodies can be up to 256 KB) and keeps the text in your own database.

Add POST /api/jobs-webhook, following the Next.js webhook example in the jobs docs:

- Read the raw body with request.text() and verify x-cloudhike-signature with lib/jobs-signature.ts before parsing anything. Answer 401 if it fails.
- Read the run id from the x-cloudhike-run-id header.
- In one transaction: INSERT the run id into handled_runs with ON CONFLICT DO NOTHING. If no row was inserted, we've already handled this run, so commit and answer 200 "already handled".
- Otherwise find the summaries row with that run_id. If the run's status is succeeded, save response.body.summary and set status 'done'. If it's failed or cancelled, set status 'failed' and save the run's error. Commit.
- If no summary has that run id, log it and answer 200. Answer 200 whenever the request was handled.

Two lines in the docs make this route worth the care:

`x-cloudhike-run-id` to skip a run you've already handled, and a primary key on `handled_runs` does exactly that, without a race between two deliveries.
The webhook receives the same JSON you'd get by polling `GET JOBS_API_URL/runs/<id>`: `status`, `attempts`, `error`, `durationMs` and `response.body`. If you'd rather not pass `callbackUrl` on every run, set a webhook URL once under **Jobs → Manage → Webhook** in the dashboard. A run's own `callbackUrl` wins over it.

CloudHike doesn't host apps yet (app hosting is coming soon), so the Next.js app goes on Vercel and the backend stays on CloudHike. Because the job calls your worker and webhook over the internet, this is also where the app comes alive:

`APP_URL` with your Vercel production address.
Want to test the webhook on its own first? **Jobs → Manage → Webhook** can send a **signed test event** to your URL. If it doesn't match a summary, your route logs it and answers 200, and either way you've seen the signature check pass.

To see what happened without leaving the editor, ask:

Use the cloudhike query_database tool to show the 10 newest summaries with their status, run_id and error, and how many rows are in handled_runs.

That runs read-only, so it's safe to let the agent explore. The **Runs** page in the dashboard shows the last 100 runs, and `GET JOBS_API_URL/runs?limit=20&status=failed` lists recent failures if you want an admin view.

A few production notes from the docs:

`retries: 0`. A summary is safe to redo, so 3 retries is fine here.` timeoutSeconds`, but your worker route also has to be allowed to run that long on your host. Check its function duration setting.`max: 5`), since each serverless instance opens its own. And a Free database with no connections for 6 hours is stopped until you click Look back at what you didn't do: you didn't set up a queue, run a worker server, write retry loops, build a webhook signing scheme or open an AI provider account. One MCP connection, and one click on the AI card, gave the agent:

Everything shows up in one dashboard and comes out of one credit balance. If you outgrow Free, **Pro is $10/month with $10 of usage credits included every month**. The details are on the [plans and billing page](https://cloudhike.dev/docs/plans-and-billing?utm_source=devto&utm_medium=article&utm_campaign=build_series_w2).

The whole build is the five prompts from Step 3 to Step 7, plus the env refresh and the optional query. Run them, swap the model, change the prompt to extract action items instead of summaries, and see how far your agent gets.

👉 **[Start free at cloudhike.dev](https://cloudhike.dev/?utm_source=devto&utm_medium=article&utm_campaign=build_series_w2)**. You get $10 in credits at signup, plus $5 more with code TUBEGURUJI5, no card required.

Next week: a **file-sharing and image-upload app** with Storage, Auth and Postgres. Users upload straight from the browser with presigned URLs, each user sees only their own files, and your agent can list the bucket without leaving the editor. Follow along so you don't miss it.

If you try this one, tell me in the comments what your agent built, or where it got stuck. I read every comment.
