Skip to content

Pre-GA Design Partner and Early Access only. Request access

Helix Docs

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.

For developers Updated Aug 5, 2026
View as Markdown

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:

// 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:

// 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 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:

// 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>
  );
}
CategoryComponents
Layout and navigationAccordion, Tabs, NavigationMenu, Menubar, ContextMenu, Breadcrumb
Forms and inputsButton, Input, Textarea, Select, Checkbox, RadioGroup, Switch, Slider, Label, Form
Data displayTable, Card, Badge, Avatar, Separator, Skeleton
FeedbackAlert, AlertDialog, Toast, Dialog, Sheet, Popover, HoverCard, Tooltip
OverlaysDropdownMenu, Command, Combobox, Calendar, DatePicker
UtilitiesAspect 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:

// 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:

/* 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:

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:

FlagResult
helix init --with-app --raw-uiRaw 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-uiNo 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:

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.

Loading search…

Jump to a section

tab to move · esc to close