# Frontend apps

> Add a standard Vite and React app in app/, call your functions from the same origin with no CORS setup, and serve it from CloudFront in production.

The `app/` directory holds your project's frontend: a standard Vite and React single-page app served from the same origin as your functions. In development one dev server runs both, so the app calls your API with plain `fetch` and no CORS setup; in production the built assets ship to S3 behind CloudFront. The directory is optional, and API-only projects don't have one.

## Project structure

`helix init --with-app` scaffolds the app:

```
app/
├── index.html              # Vite entry point
├── vite.config.ts          # Standard Vite config
├── tsconfig.json           # Extends root, targets browser/DOM
├── tailwind.config.ts      # Tailwind config with the @trayai/helix-ui preset
├── postcss.config.js       # PostCSS for Tailwind
├── src/
│   ├── main.tsx            # React root
│   ├── App.tsx             # Main app component
│   ├── index.css           # Tailwind + CSS variables for theming
│   ├── components/         # React components
│   ├── pages/              # Route pages
│   └── lib/                # Client-side utilities
└── public/                 # Static assets (favicon, images, etc.)
```

The CLI detects whether `app/` exists and adjusts the dev server and build pipeline accordingly. There is nothing Helix-specific inside the app itself.

## One origin in development

During `helix dev`, Vite runs as a plugin inside the same dev server that runs your functions. Every request hits one server:

```
localhost:3000 (dev server)
├── Function route match  → function runtime (builds and runs .ts files)
├── Static file match     → Vite (public/ and built assets)
└── No match              → Vite (serves index.html for SPA routing)
```

This means:

- The SPA calls function APIs at the same origin (`fetch('/api/v1/users')`), so no CORS configuration is needed.
- Vite HMR works normally: save a React component and see it update instantly.
- Functions hot-reload too: save a function file and the next request uses the new version.
- You open `localhost:3000` and everything is there.

The Vite config stays standard, with no proxy setup and no Helix plugins:

```typescript
// app/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  // No proxy config needed, the Helix CLI handles routing
});
```

## Calling functions from the app

Because the app and the API share an origin, API calls are plain `fetch` with relative paths:

```typescript
// app/src/lib/api.ts
const API_BASE = '/api/v1';

export async function getUsers() {
  const res = await fetch(`${API_BASE}/users`);
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json();
}

export async function createUser(data: { name: string; email: string }) {
  const res = await fetch(`${API_BASE}/users`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json();
}
```

See [functions and routing](/documentation/guides/functions-and-routing/) for the API side.

## The component library

Projects created with `helix init --with-app` include `@trayai/helix-ui` by default: a component library based on shadcn/ui, shipped as an npm package. You get:

- 40+ accessible, pre-built components (Button, Dialog, Select, Table, and more)
- Radix UI primitives underneath
- Tailwind CSS styling with CSS variable theming
- Built-in dark mode with a class-based toggle
- Automatic inheritance of your org's theme

No component source code lives in your repo; everything is imported from the package:

```tsx
// app/src/pages/Dashboard.tsx
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@trayai/helix-ui';

export function Dashboard() {
  return (
    <div className="p-6">
      <Dialog>
        <DialogTrigger asChild>
          <Button>Open Dialog</Button>
        </DialogTrigger>
        <DialogContent>
          <DialogHeader>
            <DialogTitle>Welcome</DialogTitle>
          </DialogHeader>
          <p>This is a Helix UI dialog component.</p>
        </DialogContent>
      </Dialog>
    </div>
  );
}
```

| Category | Components |
|----------|------------|
| Layout and navigation | Accordion, Tabs, NavigationMenu, Menubar, ContextMenu, Breadcrumb |
| Forms and inputs | Button, Input, Textarea, Select, Checkbox, RadioGroup, Switch, Slider, Label, Form |
| Data display | Table, Card, Badge, Avatar, Separator, Skeleton |
| Feedback | Alert, AlertDialog, Toast, Dialog, Sheet, Popover, HoverCard, Tooltip |
| Overlays | DropdownMenu, Command, Combobox, Calendar, DatePicker |
| Utilities | Aspect Ratio, Collapsible, Resizable, ScrollArea, Toggle, ToggleGroup |

### Tailwind preset

The package ships a Tailwind preset that wires up the CSS variables. Your `tailwind.config.ts` extends it:

```typescript
// app/tailwind.config.ts
import type { Config } from 'tailwindcss';
import { helixUIPreset } from '@trayai/helix-ui/tailwind';

export default {
  presets: [helixUIPreset],
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
    './node_modules/@trayai/helix-ui/**/*.js', // Include the component library
  ],
} satisfies Config;
```

The preset provides CSS variable mappings for all theme colors, border radius utilities tied to `--radius`, animation utilities, and the class-based dark mode strategy.

### Theming with CSS variables

All colors come from CSS variables defined in `app/src/index.css`: `--background`, `--foreground`, `--primary`, `--secondary`, `--muted`, `--accent`, `--destructive`, `--border`, `--input`, `--ring`, and `--radius`, each with a matching `-foreground` pair where relevant. Light values live under `:root`, dark values under `.dark`. Edit the file to change the theme:

```css
/* app/src/index.css */
@layer base {
  :root {
    --primary: 142.1 76.2% 36.3%;             /* Green instead of the default blue */
    --primary-foreground: 355.7 100% 97.3%;
    --radius: 0.75rem;                        /* Rounder corners */
  }
}
```

Quick changes: set `--primary` to your brand color, adjust `--radius` for corner rounding (0 is square, 1rem is very rounded), or swap the whole scheme by replacing all HSL values. Toggle dark mode by adding or removing the `dark` class on `document.documentElement`.

### Org-level theming

Organizations can define a default theme (colors, logo, favicon) in the dashboard under Settings, Organization, Branding. When you run `helix init --with-app`, the CLI:

1. Fetches the org theme from the Helix API
2. Injects the CSS variables into `app/src/index.css`
3. Copies the logo and favicon to `app/public/`
4. Updates `app/index.html` with the favicon reference

The org theme is applied only at project creation. The CLI never overwrites `app/src/index.css` afterward, so project-level edits stick. When the org updates its theme, pull it explicitly:

```bash
helix theme pull
```

This rewrites `app/src/index.css` with the new org colors. Review and commit the change, or discard it to keep your current theme. Theme changes are explicit commits, never silent updates.

### Other component setups

You can opt out of `@trayai/helix-ui` at init time:

| Flag | Result |
|------|--------|
| `helix init --with-app --raw-ui` | Raw shadcn/ui: components are copied into `app/src/components/ui/` as TypeScript source. You own the code, customize freely, and manage updates yourself. |
| `helix init --with-app --no-ui` | No component library at all. Install Material-UI, Chakra UI, Ant Design, or use plain Tailwind. |

In an existing project you can also run `npx shadcn@latest init` and `npx shadcn@latest add button dialog table` inside `app/` to set up raw shadcn/ui manually.

## Production serving

`helix build` builds both sides, and `helix deploy` ships them:

```bash
helix build
# .helix/build/functions/   bundled function files
# .helix/build/app/         Vite build output (index.html, assets/)

helix deploy
# Functions  → the Helix function runtime
# SPA assets → S3, served via CloudFront with caching headers
```

In production the SPA and functions share the project domain:

```
https://{project-id}.helix-app.ai/
├── /                       → SPA (index.html)
├── /dashboard              → SPA (client-side route, served as index.html)
├── /api/v1/users           → Function
├── /assets/main-abc123.js  → Static asset (Vite build output)
└── /favicon.ico            → Static asset (from app/public/)
```

Routing priority is the same as in dev: function routes first, then static files, then the SPA fallback serving `index.html` for client-side routes.

## Project shapes

- **Full-stack** (both `functions/` and `app/`): the standard setup. Functions handle server-side logic, the SPA handles the UI, and they share an origin.
- **API-only** (no `app/`): no SPA fallback, so unmatched routes return 404.
- **SPA-only** (no `functions/`): a pure static app. All routes serve `index.html`, and the app can still call external APIs directly from the browser.

---

Canonical: https://helix.tray.ai/documentation/guides/frontend/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt