Kodall
Web App Deployment

React (Vite)

Step-by-step guide to building, proxying, and deploying standalone React (Vite) applications to a Kodall instance with @kodall/kodall-deploy.

This guide covers building a standalone React Single Page Application using Vite, configuring the Vite development proxy, integrating @kodall/kodall-client, and deploying with @kodall/kodall-deploy.

This guide demonstrates a reference integration pattern for React. Kodall natively hosts any frontend application that compiles to static web assets (HTML, JavaScript, CSS, WebAssembly).

React (Vite) Live Demo

Shared Demo Mode

Deployed on Kodall instance at /react

Want an isolated database for this demo?

Currently running in shared demo mode with the public API key. Create a private 24-hour sandbox with 1-click to test creating, editing, and deleting records without interference.

https://docs-demo.kodall.io/react

Installation

Install @kodall/kodall-client and @kodall/kodall-deploy in your React project:

BASH
npm install @kodall/kodall-client
npm install -D @kodall/kodall-deploy

Configuration

Development Proxy

Configure proxying in vite.config.ts using the kodallProxy plugin from @kodall/kodall-deploy/vite to automatically sync with kodall-webapp.config.json:

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { kodallProxy } from '@kodall/kodall-deploy/vite'

export default defineConfig({
  plugins: [
    react(),
    // Option A: Automatic Vite Plugin (Recommended)
    kodallProxy() // Automatically resolves instance from default_env
  ]

  // Option B: Manual Vite Proxy (Without Helper Plugin)
  // server: {
  //   proxy: {
  //     '/auth': { target: 'https://dev.kodall.yourcompany.com', changeOrigin: true, secure: true },
  //     '/rest': { target: 'https://dev.kodall.yourcompany.com', changeOrigin: true, secure: true },
  //     '/storage': { target: 'https://dev.kodall.yourcompany.com', changeOrigin: true, secure: true }
  //   }
  // }
})
kodallProxy() automatically resolves target URLs, custom proxy_paths, and proxy routes from default_env in your configuration. To target a specific environment, pass { env: 'staging' } or explore the Local Dev Proxy Suite.

Client Setup

Create a shared instance of KodallClient:

src/lib/kodall.ts
import { KodallClient } from '@kodall/kodall-client'

export const client = new KodallClient()
Because the app runs on the same origin in production and is proxied during local development, new KodallClient() requires no hardcoded baseUrl or credentials.

Deployment Configuration (kodall-webapp.config.json)

Vite compiles production builds into the ./dist directory:

kodall-webapp.config.json
{
  "web_app_name": "React Dashboard",
  "web_app_path": "/",
  "dist_path": "./dist",
  "default_env": "dev",
  "environments": {
    "dev": {
      "type": "dev",
      "instance": "https://dev.kodall.yourcompany.com",
      "api_key": "dev-api-key-here"
    },
    "prod": {
      "type": "prod",
      "instance": "https://kodall.yourcompany.com",
      "api_key": "prod-api-key-here"
    }
  }
}
Instance Configuration: Do not use https://docs-demo.kodall.io. Enter the base URL of your own Kodall instance deployed from the Kodall Cloud Console.

Usage

Authentication & Session Restore

Support both API Key (stateless bearer) and Username/Password (cookie session), restoring the session on page refresh with an anti-flicker loading state:

src/App.tsx
import { useState, useEffect } from 'react'
import { KodallClient, isProblem } from '@kodall/kodall-client'

const getClient = (apiKey?: string) => new KodallClient({ apiKey: apiKey || undefined })

export function App() {
  const [isInitializing, setIsInitializing] = useState(true)
  const [isAuthenticated, setIsAuthenticated] = useState(false)
  const [apiKey, setApiKey] = useState('')
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')

  // Option A: Connect with API Key
  const handleConnectApiKey = async (keyInput?: string) => {
    const key = keyInput || apiKey
    if (!key.trim()) return
    const client = getClient(key)
    const result = await client.fetch('FETCH todo (key) LIMIT 1')
    if (!isProblem(result)) {
      setIsAuthenticated(true)
      localStorage.setItem('kodall_is_logged_in', 'true')
      localStorage.setItem('kodall_auth_mode', 'apikey')
      localStorage.setItem('kodall_api_key', key)
    }
  }

  // Option B: Login with Username & Password
  const handleLoginBasic = async (uInput?: string, pInput?: string) => {
    const u = uInput || username
    const p = pInput || password
    if (!u || !p) return
    const client = getClient('')
    const result = await client.auth({ user: u, password: p }, { roles: true })
    if (!isProblem(result)) {
      setIsAuthenticated(true)
      localStorage.setItem('kodall_is_logged_in', 'true')
      localStorage.setItem('kodall_auth_mode', 'basic')
    }
  }

  // Restore session on mount (anti-flicker splash)
  useEffect(() => {
    const init = async () => {
      try {
        const params = new URLSearchParams(window.location.search)
        const urlApiKey = params.get('apiKey')
        if (urlApiKey) setApiKey(urlApiKey)

        if (localStorage.getItem('kodall_is_logged_in') === 'true') {
          const mode = localStorage.getItem('kodall_auth_mode')
          if (mode === 'basic') await handleLoginBasic()
          else await handleConnectApiKey()
        }
      } finally {
        setIsInitializing(false)
      }
    }
    init()
  }, [])

  if (isInitializing) return <div className="loading">Connecting to Kodall...</div>

  return isAuthenticated ? <Dashboard /> : <LoginCard />
}

Parent-Child Entity Operations

Fetch hierarchical parent-child records in a single query and create master-detail graphs atomically:

src/App.tsx
interface TodoChild {
  item_key?: number
  description: string
  item_completed?: number | boolean
}

interface Todo {
  key: number
  title: string
  is_completed: number | boolean
  children?: { todo_todo_item?: TodoChild[] }
}

// 1. Fetch joined parent and child rows
const loadTodos = async () => {
  const result = await client.fetch<any>(`
    FETCH todo (key, title, is_completed) {
      todo_item TO id_todo (key AS item_key, description, is_completed AS item_completed)
    }
    ORDER BY key DESC
    LIMIT 100
  `)
  if (!isProblem(result) && Array.isArray(result)) {
    setTodos(groupJoinedTodos(result))
  }
}

// 2. Group flat rows into tree
const groupJoinedTodos = (rows: any[]): Todo[] => {
  const map = new Map<number, Todo>()
  for (const row of rows) {
    if (row.key == null) continue
    if (!map.has(row.key)) {
      map.set(row.key, {
        key: row.key,
        title: row.title || '',
        is_completed: row.is_completed,
        children: { todo_todo_item: [] }
      })
    }
    if (row.item_key != null) {
      map.get(row.key)!.children!.todo_todo_item!.push({
        item_key: row.item_key,
        description: row.description || '',
        item_completed: row.item_completed
      })
    }
  }
  return Array.from(map.values())
}

// 3. Create parent entity and child items
const handleCreateTodo = async (title: string, subtasks: string[]) => {
  const payload = {
    entity_name: 'todo',
    properties: { title, is_completed: 0 },
    children: {
      todo_todo_item: subtasks.map(desc => ({
        entity_name: 'todo_item',
        properties: { description: desc, is_completed: 0 }
      }))
    }
  }
  await client.create(payload)
  await loadTodos()
}

Storage API & File Uploads

Upload files directly to Kodall's storage engine using native File objects:

src/App.tsx
const handleUploadFile = async (file: File) => {
  const result = await client.uploadFile(file)
  if (!isProblem(result) && Array.isArray(result) && result.length > 0) {
    const uploaded = result[0]
    console.log('Stored File ID:', uploaded.id)
  }
}

Deployment

Package Scripts

Add build and deployment scripts to package.json:

package.json
{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "deploy": "vite build && kodall-deploy -e dev",
    "deploy:prod": "vite build && kodall-deploy -e prod"
  }
}

Executing Deployment

Execute the deployment command for your target environment:

BASH
# Deploy to development environment
npm run deploy

# Or deploy to production
npm run deploy:prod

# Or run interactively
npx kodall-deploy

kodall-deploy will package ./dist, upload the archive to Kodall storage, update the web_app entity, and execute a live HTTP health check ping.


Advanced Configuration

Subpath Mounting

If your app is hosted at a subpath (e.g. "web_app_path": "/portal"), configure base in vite.config.ts:

vite.config.ts
export default defineConfig({
  base: '/portal/',
  plugins: [react()]
})

Deployment History & Rollback

Every deployment is tracked in Kodall storage. You can inspect previous versions and instantly roll back to any prior build:

BASH
# Inspect live status across instances
npx kodall-deploy --status

# View deployment history
npx kodall-deploy --history -e prod

# Roll back to the previous deployment
npx kodall-deploy --rollback -e prod

Troubleshooting

Missing index.html in ./dist

  • Cause: vite build did not produce the expected build output.
  • Solution: Run npm run build manually and verify that ./dist/index.html is generated.

CORS Errors During Local Development

  • Cause: API requests are being sent to an absolute URL rather than using the Vite proxy.
  • Solution: Ensure client instantiation uses same-origin relative URLs (new KodallClient()).