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, 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 awayPOST /api/jobs/summarize (your worker) → AI model → summary as JSONPOST /api/jobs-webhook → signature check → duplicate check → summary savedGET /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.
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. 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 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 and 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 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.
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. 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.