# Frontend Dev Without a Browser

How we built a visual development workflow where an AI agent edits React code,
builds to S3, and uses a remote browser API to see what it made. No Chrome
installed.

[<- All Posts](/blog)

## The Problem

We run an AI coding agent on EC2 that builds and iterates on React games and
components. The typical frontend dev loop is: edit code, check the browser,
repeat. But the agent runs on EC2. It does not have a monitor, a desktop
environment, or a reason to install a local Chrome.

Headless Chrome eats hundreds of megabytes of RAM before it does useful work.
On small EC2 instances, that leaves too little room for the agent process, the
LLM context, and builds.

The fix was to remove the browser from the agent host and make the visual loop
remote.

## The Architecture

The loop is simple:

1. Edit: the agent modifies source files in a git worktree.
2. Build: the app compiles to static HTML, JavaScript, and CSS.
3. Upload: `aws s3 sync` pushes the build output to S3.
4. Observe: Riddle loads the CloudFront URL, captures screenshots, and records
   console output.
5. Post: the agent shares the screenshot, findings, and next step in the review
   thread.
6. Repeat.

Each round can stay in the 10-15 second range. The agent does not need a local
browser, a dev server, or a tunnel.

## The Focused Build Trick

A full site build is often too slow for a visual iteration loop. If the agent is
editing one component, build only that component with a generated Vite shell:

```jsx
// Generated entry point: src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import Component from '../../actual/path/to/SnakeFight.jsx';
import '../../actual/path/to/SnakeFight.css';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode><Component /></React.StrictMode>
);
```

The generated shell lives in a temp directory with a small `vite.config.js` and
`index.html`. It symlinks the project `node_modules` instead of installing
dependencies again:

```sh
ln -sf /path/to/project/node_modules /tmp/preview-focused-1234/node_modules
```

## Why S3 Instead of a Dev Server

The obvious approach is to run `vite dev` and point a browser at
`localhost:5173`. That adds lifecycle problems: the dev server needs to stay up,
the browser needs network access to the host, and the preview disappears when
the process stops.

The upload step is boring by design:

```sh
aws s3 sync ./dist s3://bucket/preview/repo/latest --delete
```

The preview becomes a normal HTTPS URL that can be loaded by Riddle, referenced
in artifacts, and inspected later.

## CloudFront for Asset Rewriting, Not Caching

Vite apps can often use relative assets. Static Next.js exports are trickier
because they can emit absolute asset paths such as
`/_next/static/chunks/abc123.js`.

When a browser loads `/preview/repo/label/index.html` and the page requests
`/_next/static/chunks/abc123.js`, a CloudFront Function can use the `Referer`
header to rewrite the request to
`/preview/repo/label/_next/static/chunks/abc123.js`.

```js
function handler(event) {
  var request = event.request;
  var uri = request.uri;

  if (!uri.startsWith('/_next/') && !uri.startsWith('/assets/')) {
    return request;
  }

  var referer = request.headers.referer && request.headers.referer.value;
  var match = referer && referer.match(/\/preview\/([^/]+)\/([^/]+)\//);
  if (match) {
    request.uri = '/preview/' + match[1] + '/' + match[2] + uri;
  }

  return request;
}
```

Caching stays disabled. The point is URL rewriting and stable public previews,
not long-lived cached assets.

## CORS

CORS is mostly not the problem here. The browser sees a same-origin CloudFront
site. Static JavaScript, CSS, images, and fonts all load from the same preview
origin after rewriting.

## Observing Without a Browser

The useful part is not just a screenshot. The agent can add runtime telemetry to
the app, then have Riddle read that telemetry in the remote browser:

```js
window.__gameTelemetry = {
  fps,
  activeEffects,
  score,
  playerPosition,
};
```

Then a proof script can capture both screenshots and state:

```js
const telemetry = await page.evaluate(() => window.__gameTelemetry);
await page.screenshot({ path: 'after-change.png' });
```

The agent can verify state, visual output, console health, and network behavior
without installing Chrome locally.

## Authenticated Pages

Previewing authenticated pages usually means injecting test cookies or
`localStorage` values before running the visual proof. Cookies can be set before
navigation. `localStorage` needs the browser on the target origin first.

```js
await context.addCookies([
  {
    name: 'session',
    value: 'test-session',
    domain: 'preview.example.com',
    path: '/',
    httpOnly: true,
    secure: true,
  },
]);
```

For login flows, Riddle can run normal steps or a Playwright script:

```json
{
  "steps": [
    { "type": "goto", "url": "https://preview.example.com/login" },
    { "type": "fill", "selector": "#email", "value": "agent@example.com" },
    { "type": "fill", "selector": "#password", "value": "password" },
    { "type": "click", "selector": "button[type=submit]" }
  ]
}
```

## Discord as the UI

The human-facing review surface can be a normal thread. After every round, the
agent posts:

- the screenshot;
- what changed and why;
- key telemetry such as FPS, error count, and state values;
- what it plans to do next.

Those posts become a lightweight visual change log.

## The RAM Math

A small EC2 agent host should spend memory on the agent, tools, and build
process, not on a local browser. Moving the browser to Riddle keeps the host
lean while still giving the agent visual feedback.

## The Gotchas

- Absolute asset paths need rewriting for mounted static previews.
- Symlinked `node_modules` can confuse binary resolution, so call the local
  binary when possible.
- S3 previews need `--delete` to avoid stale JavaScript and CSS.
- Every visual loop needs durable artifacts, not only a final summary.

## The Full Stack

- Agent: AI coding agent on EC2.
- Source control: git worktrees.
- Build: focused Vite shell, static Next.js export, or full project build.
- Hosting: S3 behind CloudFront.
- Browser: Riddle API for screenshots, scripts, console capture, and artifacts.
- Human interface: review thread with per-round screenshots.
- State: persistent workspace storage.
- CI: preflight build before pushing and opening a draft PR.

## Is This Actually Better?

For a human on a laptop, a local dev server and browser are still simple. For a
remote coding agent, the remote-browser loop removes browser installation,
tunnel management, and local sandbox risk while producing durable artifacts.

## Try It

Use Riddle to load the deployed preview, capture a screenshot, collect console
output, and compare what happened against the specific change the agent was
trying to prove.

