Kodall
Web App Deployment

Next.js (Static Export)

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

This guide covers configuring a Next.js application for static HTML/JS export (output: 'export'), setting up local development rewrites, integrating @kodall/kodall-client, and deploying with @kodall/kodall-deploy.

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

Next.js (Static Export) Live Demo

Shared Demo Mode

Deployed on Kodall instance at /next

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

Installation

Install @kodall/kodall-client and @kodall/kodall-deploy in your Next.js project:

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

Configuration

Development Proxy

Enable Next.js static export using output: 'export'. Set up rewrites using @kodall/kodall-deploy's getNextRewrites() helper to automatically forward /auth, /rest, and /storage requests to your target Kodall instance during next dev:

next.config.mjs
import { getNextRewrites } from '@kodall/kodall-deploy'

/** @type {import('next').NextConfig} */
const nextConfig = {
  // 1. Enable static export (outputs to ./out)
  output: 'export',

  // Option A: Automatic Helper (Recommended)
  async rewrites() {
    return getNextRewrites() // Automatically resolves instance from default_env
  }

  // Option B: Manual Rewrites Array (Without Helper Package)
  // async rewrites() {
  //   const BACKEND_URL = 'https://dev.kodall.yourcompany.com'
  //   return [
  //     { source: '/auth/:path*', destination: `${BACKEND_URL}/auth/:path*` },
  //     { source: '/rest/:path*', destination: `${BACKEND_URL}/rest/:path*` },
  //     { source: '/storage/:path*', destination: `${BACKEND_URL}/storage/:path*` }
  //   ]
  // }
}

export default nextConfig
getNextRewrites() automatically maps all required API endpoints and custom proxy_paths from default_env in 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)

Next.js exports static builds into the ./out directory:

kodall-webapp.config.json
{
  "web_app_name": "Next.js Portal",
  "web_app_path": "/",
  "dist_path": "./out",
  "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

Any component using client hooks or browser state must declare 'use client' at the top:

src/app/page.tsx
'use client'

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

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

  // Option A: API Key
  const handleConnectApiKey = async () => {
    const client = new KodallClient({ apiKey })
    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', apiKey)
    }
  }

  // Option B: Username & Password
  const handleLoginBasic = async () => {
    const client = new KodallClient()
    const result = await client.auth({ user: username, password }, { 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>Connecting to Kodall...</div>

  return isAuthenticated ? <AppShell /> : <LoginForm />
}

Parent-Child Entity Operations

Fetch hierarchical parent-child entities in a single query and group them into a tree:

src/app/page.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 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
`)

// 2. Group flat 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: [] }
      })
    }
    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 atomically
await client.create({
  entity_name: 'todo',
  properties: { title: 'Next.js App Deployment', is_completed: 0 },
  children: {
    todo_todo_item: [
      { entity_name: 'todo_item', properties: { description: 'Configure export', is_completed: 1 } },
      { entity_name: 'todo_item', properties: { description: 'Deploy build', is_completed: 0 } }
    ]
  }
})

Storage API & File Uploads

Upload files directly to Kodall's storage engine:

src/app/page.tsx
const handleUploadFile = async (file: File) => {
  const client = new KodallClient({ apiKey })
  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": "next dev",
    "build": "next build",
    "deploy": "next build && kodall-deploy -e dev",
    "deploy:prod": "next 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 ./out, package the build directory, 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 basePath in next.config.mjs:

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  basePath: '/portal'
}

export default nextConfig

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

  • Cause: output: 'export' was omitted from next.config.mjs.
  • Solution: Ensure next.config.mjs contains output: 'export' so that next build emits a static ./out directory.

Rewrites Not Forwarding API Calls

  • Cause: Incorrect destination URL or missing path wildcards in next.config.mjs.
  • Solution: Ensure destination paths include :path* (e.g. destination: 'https://.../rest/:path*').