A Vite React app runs entirely in the browser, which means it cannot safely hold API keys or call Agentuity services directly. The pattern here pairs the SPA with a thin server.ts that handles /api/* routes, holds secrets, and proxies the Vite dev server in development. In deployed builds the same process serves the static bundle.
Create the Framework App
npm create vite@latest my-vite-app -- --template react-ts
cd my-vite-appThen add Agentuity and create a small server boundary:
npm install -D @agentuity/cli
npx agentuity project import --validate-only
npx agentuity project importUse the package manager your Vite app uses. The CLI install is project-local: -D or --dev records @agentuity/cli in devDependencies, which keeps the CLI version in the app's lockfile. Bun's equivalent is lowercase -d.
| Package manager | Add local CLI | Run local CLI | Undo local install |
|---|---|---|---|
| npm | npm install -D @agentuity/cli | npx agentuity ... | npm uninstall @agentuity/cli |
| Bun | bun add -d @agentuity/cli | bunx agentuity ... | bun remove @agentuity/cli |
| pnpm | pnpm add -D @agentuity/cli | pnpm exec agentuity ... | pnpm remove @agentuity/cli |
| Yarn | yarn add -D @agentuity/cli | yarn exec agentuity ... | yarn remove @agentuity/cli |
A global install is optional. It only makes bare agentuity ... work outside a project; it does not import or register the app. You can switch back by adding the dev dependency again and using the package-manager exec wrapper. The global install can stay installed, or you can remove it with the package manager that created it. See Local versus global CLI for the trade-offs.
Agentuity does not scaffold Vite + React from the create flow today.
| File | Purpose |
|---|---|
src/App.tsx | Browser UI |
server.ts | Bun server: handles /api/* and serves static files in deployed builds |
vite.config.ts | Vite configuration |
Add to an Existing App
Install the CLI as a dev dependency, then import the project:
npm install -D @agentuity/cli
npx agentuity project import --validate-only # dry run: checks config only
npx agentuity project import # registers the project and writes agentuity.jsonUse the package-manager table above if the app is not npm-based.
agentuity dev can supply AGENTUITY_SDK_KEY to the process that runs your dev script.
See Add Agentuity to an existing app for validation, import, the Invalid project folder deploy case, and first deploy.
What Agentuity adds
| Area | Vite keeps | Agentuity adds |
|---|---|---|
| routing | browser SPA routes | a server boundary for secrets and /api/* routes |
| local dev | Vite dev server | agentuity dev supplies service credentials |
| build | Vite browser bundle | agentuity build runs the build and packages the Bun server entry |
| deploy | dist/server.js plus static assets | agentuity deploy ships the packaged app |
Add a Server Boundary
Browser code cannot use AGENTUITY_SDK_KEY or provider secrets safely. Move those calls into server.ts and call them from React over fetch.
npm install @agentuity/aigateway @agentuity/keyvalue zod
npm install -D @types/bunThe server below handles POST /api/translate, stores session history in KV, calls AI Gateway with the project SDK key, and falls back to a Vite dev proxy in development.
import { AIGatewayClient } from '@agentuity/aigateway';
import { KeyValueClient } from '@agentuity/keyvalue';
import { z } from 'zod';
const DEFAULT_MODEL = 'openai/gpt-5.4-mini';
const HISTORY_NAMESPACE = 'translation-history';
const SESSION_COOKIE = 'agentuity_session';
const SESSION_TTL = 60 * 60 * 24 * 30; // 30 days
const PORT = Number.parseInt(process.env.PORT ?? '3000', 10);
const NODE_ENV = Bun.env.NODE_ENV ?? process.env.NODE_ENV;
interface HistoryEntry {
readonly model: string;
readonly sessionId: string;
readonly text: string;
readonly timestamp: string;
readonly toLanguage: string;
readonly translation: string;
}
interface HistoryState {
readonly history: readonly HistoryEntry[];
readonly translationCount: number;
}
const requestSchema = z.object({
text: z.string(),
toLanguage: z.string(),
model: z.string().optional(),
});
const kv = new KeyValueClient();
const gateway = new AIGatewayClient();
function getSessionId(request: Request): string {
const cookie = request.headers.get('cookie') ?? '';
const match = cookie.match(/(?:^|; )agentuity_session=([^;]+)/);
return match?.[1] ? decodeURIComponent(match[1]) : `sess_${crypto.randomUUID()}`;
}
Bun.serve({
port: PORT,
hostname: '0.0.0.0',
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/api/translate' && request.method === 'POST') {
const body: unknown = await request.json();
const parsed = requestSchema.safeParse(body);
if (!parsed.success) {
return Response.json({ error: 'text and toLanguage are required' }, { status: 400 });
}
const { text, toLanguage } = parsed.data;
const model = parsed.data.model ?? DEFAULT_MODEL;
const sessionId = getSessionId(request);
const result = await gateway.completeText({
model,
messages: [{ role: 'user', content: `Translate to ${toLanguage}:\n\n${text}` }],
});
if (!result.hasText) {
return Response.json({ error: 'model returned no text' }, { status: 502 });
}
const translation = result.text;
// Append to per-session history, keep last 5 entries
const previous = await kv.get<HistoryState>(HISTORY_NAMESPACE, sessionId);
const history = [
...(previous.exists ? previous.data.history : []),
{
model,
sessionId,
text,
timestamp: new Date().toISOString(),
toLanguage,
translation,
},
].slice(-5);
const translationCount = (previous.exists ? previous.data.translationCount : 0) + 1;
await kv.set(HISTORY_NAMESPACE, sessionId, { history, translationCount }, { ttl: SESSION_TTL });
return Response.json(
{ sessionId, translation, history, model, translationCount },
{
headers: {
'Set-Cookie': [
`${SESSION_COOKIE}=${encodeURIComponent(sessionId)}`,
'HttpOnly',
'Path=/',
'SameSite=Lax',
`Max-Age=${SESSION_TTL}`,
...(NODE_ENV === 'production' ? ['Secure'] : []),
].join('; '),
},
},
);
}
// Static fallback for deployed builds. Vite output is co-located with server.js
if (NODE_ENV === 'production') {
const pathname = url.pathname === '/' ? '/index.html' : url.pathname;
const file = Bun.file(import.meta.dir + pathname);
// Fall back to index.html so the SPA router handles the path
const body = (await file.exists()) ? file : Bun.file(import.meta.dir + '/index.html');
return new Response(body);
}
// Development: proxy everything else to the Vite dev server
const viteRes = await fetch(`http://127.0.0.1:5173${url.pathname}${url.search}`, {
method: request.method,
headers: request.headers,
body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined,
});
return new Response(viteRes.body, {
status: viteRes.status,
headers: viteRes.headers,
});
},
});The local script below runs this file with Bun so Bun.serve, Bun.file, and HMR work as shown. Use the model id exactly as it appears in the AI Gateway model catalog. Use a provider SDK directly only when the server route depends on provider-specific APIs.
Call the Server from React
async function translate(text: string, toLanguage: string): Promise<string> {
const response = await fetch('/api/translate', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text, toLanguage }),
});
if (!response.ok) {
throw new Error(`Translation failed: ${response.status}`);
}
const data = await response.json();
return data.translation;
}Run Locally
A Vite + React app needs two processes during development: Vite for the SPA and server.ts for the API. The default dev script from create-vite only runs Vite, so configure package.json to run both.
The recommended pattern uses bun run --parallel to run both scripts simultaneously without a third-party dependency:
{
"scripts": {
"dev": "agentuity dev --script dev:start",
"dev:start": "bun run --parallel dev:vite dev:server",
"dev:vite": "vite --host 127.0.0.1",
"dev:server": "bun --hot server.ts",
"build": "tsc -b && vite build && bun build server.ts --target=bun --outfile=dist/server.js",
"start": "NODE_ENV=production bun dist/server.js",
"deploy": "agentuity deploy"
}
}npm run devagentuity dev runs the wrapper dev:start script, which uses Bun's parallel runner to launch both Vite and server.ts in the same process group. The CLI supplies AGENTUITY_SDK_KEY and PORT=3000 when the linked project key is available. server.ts reads PORT and listens on 3000; Vite uses port 5173 internally.
Do not swap dev:server to node --watch server.ts unless you also rewrite server.ts for Node APIs. This example uses Bun.serve and Bun.file, so run it with Bun locally and in the deployed start script.
Smoke-test the API route on port 3000:
curl http://localhost:3000/api/translate \
-H "content-type: application/json" \
-d '{"text":"Hello","toLanguage":"Spanish"}'In development, server.ts proxies non-API requests to Vite on port 5173. Your browser hits port 3000 for everything, including HMR. vite.config.ts does not need a proxy entry because the proxy runs in the other direction.
Deployed Environment Variables
Configure the variables your deployed app needs. Direct AIGatewayClient calls use AGENTUITY_SDK_KEY; the model ID in this example is normal app configuration or request input.
| Variable | Used by |
|---|---|
AGENTUITY_SDK_KEY | Agentuity service clients and @agentuity/aigateway in server.ts |
DATABASE_URL | database routes in server.ts |
AWS_* | object storage routes in server.ts; see Object Storage for exact variables |
Validate Before Deploy
Run the framework build when you want Vite/server-only feedback, then run the Agentuity build validator before deploying. agentuity build runs the build script during packaging and writes .agentuity/launch.json.
npm run build
npx agentuity build
cat .agentuity/launch.jsonVite + React apps are detected as the generic vite adapter. Inspect .agentuity/launch.json to confirm the start command points at dist/server.js, not at vite preview or a static _serve.js helper. If your start command has an env prefix such as NODE_ENV=production bun dist/server.js, trust processes[0].command over the runtime.name hint.
Deploy only after the framework build, Agentuity build, and a local packaged validation of / and /api/translate pass:
npx agentuity deploy --confirmvite build only bundles the browser code. The build script must also run bun build server.ts --target=bun --outfile=dist/server.js so the deployed process has a compiled server entry. Missing this step means Agentuity will have no server to start.
Common Gotchas
| Symptom | Fix |
|---|---|
AGENTUITY_SDK_KEY appears in browser bundle | Move any Agentuity client code into server.ts |
/api/* routes work locally but 404 after deploy | Check .agentuity/launch.json: the start command must run dist/server.js, not vite preview |
/api/* routes return SPA HTML after packaging | launch.json is starting a static helper such as _serve.js; it is not running server.ts |
vite preview used as the deployed server | vite preview is for local build verification only; it has no /api/* handler. See Vite backend integration |
Missing dist/server.js after build | Add bun build server.ts --target=bun --outfile=dist/server.js to the build script |
| Model call returns an auth error | Verify AGENTUITY_SDK_KEY is available to server.ts. See AI Gateway |
Framework Docs
- Vite Backend Integration: how to connect Vite development to a backend server
- Vite Static Deploy:
vite previewis for local build verification only, not for serving deployed apps
Next Steps
- App Configuration: configure
agentuity.json, environment variables, and scripts - Build Configuration: inspect launch metadata and packaging output
- AI Gateway: route supported model calls through Agentuity
- Key-Value Storage: store session state and cache data from server code