Kodall
Web App Deployment

SvelteKit

Step-by-step guide to building, proxying, and deploying static SvelteKit applications to a Kodall instance with @kodall/kodall-deploy.

This guide covers configuring a SvelteKit application for static SPA generation using @sveltejs/adapter-static, setting up the development proxy, integrating @kodall/kodall-client, and deploying with @kodall/kodall-deploy.

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

SvelteKit Live Demo

Shared Demo Mode

Deployed on Kodall instance at /svelte

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

Installation

Install @kodall/kodall-client, @kodall/kodall-deploy, and @sveltejs/adapter-static in your SvelteKit project:

BASH
npm install @kodall/kodall-client
npm install -D @kodall/kodall-deploy @sveltejs/adapter-static

Configuration

Development Proxy

Configure @sveltejs/adapter-static with an index.html fallback in svelte.config.js:

svelte.config.js
import adapter from '@sveltejs/adapter-static'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'

export default {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: 'index.html'
    })
  }
}

Configure Vite's development proxy in vite.config.ts:

vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'
import { kodallProxy } from '@kodall/kodall-deploy/vite'

export default defineConfig({
  plugins: [
    sveltekit(),
    // 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 kodall-webapp.config.json. 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)

SvelteKit static builds are emitted to ./build:

kodall-webapp.config.json
{
  "web_app_name": "Svelte App",
  "web_app_path": "/",
  "dist_path": "./build",
  "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/routes/+page.svelte
<script lang="ts">
  import { onMount } from 'svelte'
  import { KodallClient, isProblem } from '@kodall/kodall-client'

  let isInitializing = true
  let isAuthenticated = false
  let authMode: 'apikey' | 'basic' = 'apikey'
  let apiKey = ''
  let username = ''
  let password = ''
  let loading = false
  let authError = ''

  function getClient(customKey?: string): KodallClient {
    const keyToUse = customKey !== undefined ? customKey : (authMode === 'apikey' ? apiKey : undefined)
    return new KodallClient({ apiKey: keyToUse || undefined })
  }

  // Option A: API Key
  async function handleConnectApiKey(keyInput?: string) {
    const targetKey = keyInput || apiKey
    if (!targetKey.trim()) return
    loading = true
    authError = ''
    try {
      const client = getClient(targetKey)
      const result = await client.fetch('FETCH todo (key, title, is_completed) LIMIT 1')
      if (isProblem(result)) {
        authError = `Authentication failed: ${(result as any).detail}`
      } else {
        isAuthenticated = true
        authMode = 'apikey'
        localStorage.setItem('kodall_is_logged_in', 'true')
        localStorage.setItem('kodall_auth_mode', 'apikey')
        localStorage.setItem('kodall_api_key', targetKey)
      }
    } finally {
      loading = false
    }
  }

  // Option B: Username / Password
  async function handleLoginBasic(uInput?: string, pInput?: string) {
    const u = uInput || username
    const p = pInput || password
    if (!u || !p) return
    loading = true
    authError = ''
    try {
      const client = getClient('')
      const result = await client.auth({ user: u, password: p }, { roles: true })
      if (isProblem(result)) {
        authError = (result as any).detail || 'Login failed'
      } else {
        isAuthenticated = true
        authMode = 'basic'
        localStorage.setItem('kodall_is_logged_in', 'true')
        localStorage.setItem('kodall_auth_mode', 'basic')
        localStorage.setItem('kodall_username', u)
        localStorage.setItem('kodall_password', p)
      }
    } finally {
      loading = false
    }
  }

  // Restore session on mount (anti-flicker splash)
  onMount(async () => {
    try {
      const params = new URLSearchParams(window.location.search)
      const urlApiKey = params.get('apiKey') || params.get('api_key')
      const urlUser   = params.get('user') || params.get('username')
      const urlPass   = params.get('password') || params.get('pass')

      if (urlApiKey) { apiKey = urlApiKey; localStorage.setItem('kodall_api_key', urlApiKey) }
      if (urlUser)   { username = urlUser;  localStorage.setItem('kodall_username', urlUser) }
      if (urlPass)   { password = urlPass;  localStorage.setItem('kodall_password', urlPass) }

      if (!urlApiKey && !urlUser && !urlPass) {
        apiKey   = localStorage.getItem('kodall_api_key')  || ''
        username = localStorage.getItem('kodall_username') || ''
        password = localStorage.getItem('kodall_password') || ''
      }

      const isLoggedIn = localStorage.getItem('kodall_is_logged_in') === 'true'
      const savedMode  = localStorage.getItem('kodall_auth_mode')

      if (isLoggedIn) {
        const key  = urlApiKey || apiKey  || localStorage.getItem('kodall_api_key')
        const user = urlUser   || username || localStorage.getItem('kodall_username')
        const pass = urlPass   || password || localStorage.getItem('kodall_password')

        if (savedMode === 'basic' && user && pass) {
          await handleLoginBasic(user, pass)
        } else if (key) {
          await handleConnectApiKey(key)
        }
      }
    } finally {
      isInitializing = false
    }
  })
</script>

{#if isInitializing}
  <p>Connecting to Kodall...</p>
{:else if !isAuthenticated}
  <section class="auth-card">
    <button on:click={() => handleConnectApiKey()}>Connect with API Key</button>
    <button on:click={() => handleLoginBasic()}>Sign In with Password</button>
  </section>
{:else}
  <div class="dashboard">...</div>
{/if}

Parent-Child Entity Operations

Fetch hierarchical parent-child relationships and create nested graphs in a single round-trip:

src/routes/+page.svelte
<script lang="ts">
  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[] }
  }

  let todos: Todo[] = []

  // 1. Group flat joined rows into tree
  function 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: [] } })
      }
      const parent = map.get(row.key)!
      if (row.item_key != null) {
        parent.children!.todo_todo_item!.push({
          item_key: row.item_key,
          description: row.description || '',
          item_completed: row.item_completed
        })
      }
    }
    return Array.from(map.values())
  }

  // 2. Fetch joined records
  async function loadTodos(client: any) {
    const result = await client.fetch(`
      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)) {
      todos = groupJoinedTodos(result)
    }
  }

  // 3. Create parent entity and child items
  async function handleCreateTodo(client: any, 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(client)
  }
</script>

Storage API & File Uploads

Upload files directly using Kodall's storage API:

src/routes/+page.svelte
<script lang="ts">
  async function handleUploadFile(client: any, 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)
    }
  }
</script>

Deployment

Package Scripts

Add build and deployment scripts to package.json:

package.json
{
  "scripts": {
    "dev": "vite dev",
    "build": "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 validate ./build, package the static assets, 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 deployed to a subpath (e.g. "web_app_path": "/portal"), configure paths.base in svelte.config.js:

svelte.config.js
export default {
  kit: {
    paths: {
      base: '/portal'
    }
  }
}

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 ./build

  • Cause: @sveltejs/adapter-static was not configured with fallback: 'index.html'.
  • Solution: Verify svelte.config.js includes fallback: 'index.html' in the adapter options.

404 on Direct Route Refresh in Production

  • Cause: Static SPA routing requires all client routes to fall back to index.html.
  • Solution: Ensure your adapter build includes the fallback index.html.