Skip to content

The app server

Some things must not happen in your visitors’ browsers: calling an API with a secret key, sending an email, receiving a webhook. For that, your app can have its own server, with routes under /api. It only exists if your app needs it, and Highwind takes care of all its configuration.

Calling a service with a key

AI (OpenAI, Mistral…), payments, sending emails, maps: the key stays on the server and never reaches the browser.

Sending messages

A contact form by email, a notification in Slack, a sign-up added to a Google Sheets spreadsheet.

Receiving webhooks

An external service (payments, forms, automation) tells your app that something happened.

Reaching an API closed to browsers

Some APIs refuse calls coming straight from a web page: the server calls them instead.

If your app only needs to keep data for the visitor, or a database with user accounts, a server isn’t necessarily needed: see Where data goes.

An app has no server at first. When a feature needs one, the agent adds it itself (the “Adds a server to the app” step in the thread), or by wiring in a connector that needs one.

  1. Highwind creates the worker/index.ts file from its template and wires all the configuration. The agent never writes that configuration itself.

  2. The agent adds the routes your app needs to worker/index.ts, for example POST /api/contact.

  3. Your app’s front end calls these routes with relative addresses (fetch("/api/contact")), which work in the preview as well as once published.

  4. If a route needs a key, the agent asks you for it with a card in the chat: see Secrets.

You can also ask for it directly:

Add a contact form that emails me each message at contact@myworkshop.com, with
field validation.
Add a route that writes a product description with OpenAI from the name and
features the user enters.
  • It receives every /api/* request. Everything else (pages, images, scripts) is your app’s front end.
  • It runs on the Cloudflare Workers engine, in the preview as well as once published: what works in the preview works live. This engine provides the standard web APIs (fetch, Request, Response, crypto.subtle), but no file system, no long-running process and no Express.
  • An unknown route answers a 404 error in JSON. An uncaught error is written to the logs and the visitor gets a generic 500 error, without technical details.
  • An app without a server doesn’t change: it stays a static site. If the worker/index.ts file is deleted, the app becomes static again.

The server declares its routes in a routes table (“METHOD /api/path”), reads its secrets from env and answers with the json() helper:

worker/index.ts (excerpt)
export interface Env {
RESEND_API_KEY: string;
}
const routes: Record<string, Handler> = {
"GET /api/health": () => json({ ok: true }),
"POST /api/contact": async (request, env) => {
const { email, message } = await request.json();
if (!email || !message) return json({ error: "Missing required fields" }, 400);
if (!env.RESEND_API_KEY) return json({ error: "Resend key not added yet" }, 503);
// … send the email with env.RESEND_API_KEY
return json({ ok: true });
},
};

When a route doesn’t do what you expect, the logs show what happened: the server’s console.log output and errors, with file and line.

  1. In the preview bar, click the Server logs icon. The panel opens at the bottom of the preview.

  2. Pick the tab:

    • Preview: your preview’s server (the console.log output and errors of the /api routes, and Vite’s messages), refreshed continuously. Error lines are shown in red.
    • Live: your published app’s server, over the last 24 hours, a few seconds late. Each /api call shows up with its status and duration, along with console.log output and errors. Neither headers nor visitors’ IP addresses are shown.
  3. If you don’t understand the error, ask the agent to fix it: it reads these logs itself (the read_logs tool), in the preview or live.

The contact form returns an error in the published app although it works in the
preview. Check the live logs and fix it.

Anyone can call your app’s /api routes, not just its pages. So the agent applies these rules to every route:

  • Validate every input and answer a clear error with the right status code (400 for invalid input, for example).
  • Never return a secret or the technical details of an error to the browser.
  • Limit routes that send messages or spend money: recipient fixed on the server, required fields, signed-in user when the app has a database.
  • Read secrets on the server only, never in the front-end code (src/).

Highwind adds its own safeguards on published apps:

Safeguard Value
/api requests per visitor and per app 120 per minute, then a 429 error (“try again in a minute”)
/api requests per app, all visitors combined 3,000 per minute
Compute time per request 1 second of CPU (waiting on external APIs doesn’t count)
Outgoing calls per request 200

Your app’s pages and static files aren’t affected by these limits.