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.
Deployed on Kodall instance at /next
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.
Install @kodall/kodall-client and @kodall/kodall-deploy in your Next.js project:
npm install @kodall/kodall-client
npm install -D @kodall/kodall-deploy
pnpm add @kodall/kodall-client
pnpm add -D @kodall/kodall-deploy
yarn add @kodall/kodall-client
yarn add -D @kodall/kodall-deploy
bun add @kodall/kodall-client
bun add -d @kodall/kodall-deploy
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:
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.Create a shared instance of KodallClient:
import { KodallClient } from '@kodall/kodall-client'
export const client = new KodallClient()
new KodallClient() requires no hardcoded baseUrl or credentials.Next.js exports static builds into the ./out directory:
{
"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"
}
}
}
https://docs-demo.kodall.io. Enter the base URL of your own Kodall instance deployed from the Kodall Cloud Console.Any component using client hooks or browser state must declare 'use client' at the top:
'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 />
}
Fetch hierarchical parent-child entities in a single query and group them into a tree:
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 } }
]
}
})
Upload files directly to Kodall's storage engine:
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)
}
}
Add build and deployment scripts to package.json:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"deploy": "next build && kodall-deploy -e dev",
"deploy:prod": "next build && kodall-deploy -e prod"
}
}
Execute the deployment command for your target environment:
# 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.
If your app is deployed to a subpath (e.g. "web_app_path": "/portal"), configure basePath in next.config.mjs:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
basePath: '/portal'
}
export default nextConfig
Every deployment is tracked in Kodall storage. You can inspect previous versions and instantly roll back to any prior build:
# 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
Missing index.html in ./outoutput: 'export' was omitted from next.config.mjs.next.config.mjs contains output: 'export' so that next build emits a static ./out directory.Rewrites Not Forwarding API Callsnext.config.mjs.:path* (e.g. destination: 'https://.../rest/:path*').React (Vite)
Step-by-step guide to building, proxying, and deploying standalone React (Vite) applications to a Kodall instance with @kodall/kodall-deploy.
Angular (17+)
Step-by-step guide to building, proxying, and deploying modern Angular (17+) applications to a Kodall instance with @kodall/kodall-deploy.