Skip to main content

Web Platform

The Web platform is the primary deployment target for GreenhouseAdmin. This guide covers building and deploying the application using either the modern Wasm target or the legacy JavaScript target.

Overview

GreenhouseAdmin supports two web compilation targets:
  • Wasm (WebAssembly): Modern, faster runtime using Kotlin/Wasm - Recommended
  • JS (JavaScript): Legacy target for older browser support
The Wasm target provides better performance and is recommended for production deployments to modern browsers.

Development

Running the Development Server

HTTP Client Configuration

The web targets use the Ktor JS engine for network requests:
composeApp/build.gradle.kts
val wasmJsMain by getting {
    dependencies {
        // Ktor - JS engine for Wasm
        implementation(libs.ktor.client.js)
    }
}

jsMain.dependencies {
    // Ktor - JS engine for JavaScript
    implementation(libs.ktor.client.js)
}

Production Build

Building for Production

1

Upgrade Yarn Lock Files

After any dependency changes, update the Yarn lock files for both targets:
./gradlew kotlinWasmUpgradeYarnLock
Always run these commands after modifying dependencies in build.gradle.kts or libs.versions.toml.
2

Build Production Distribution

Build the optimized production bundle:
./gradlew :composeApp:wasmJsBrowserDistribution
Output location: composeApp/build/dist/wasmJs/productionExecutable/
3

Configure API Base URL

Set the API endpoint using local.properties:
local.properties
API_BASE_URL=https://api.example.com/api/v1/
This value is injected into the application via BuildKonfig at compile time.

Deployment

Nginx Configuration

The project includes a production-ready Nginx configuration with optimizations for Wasm applications.
  • Wasm MIME Type: Properly configured application/wasm type
  • COOP/COEP Headers: Required for SharedArrayBuffer support (needed by Skiko/Compose)
  • Smart Caching: Aggressive caching for hashed assets, revalidation for entry points
  • SPA Routing: Fallback to index.html for client-side routing
  • Gzip Compression: Including .wasm files
  • Security Headers: XSS protection, frame options, content type sniffing prevention
nginx.conf
# Required for SharedArrayBuffer support in Compose Multiplatform
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;

# Add WASM MIME type
types {
    application/wasm wasm;
}
Without these headers, Wasm applications using Skiko (Compose’s rendering engine) will fail to load.
nginx.conf
# index.html: NEVER cache aggressively (allows updates)
location = /index.html {
    add_header Cache-Control "no-cache" always;
}

# Entry point JS/CSS: revalidate always
location ~* ^/(composeApp\.js|styles\.css)$ {
    add_header Cache-Control "no-cache" always;
}

# Hashed chunks and WASM: cache forever (immutable)
location ~* \.wasm$ {
    expires 1y;
    add_header Cache-Control "public, immutable" always;
}
The complete configuration is available at nginx.conf in the project root.

Docker Deployment

The project includes a multi-stage Dockerfile for containerized deployments.
1

Build Docker Image

Build the production image with API configuration:
docker build \
  --build-arg API_BASE_URL=https://api.example.com/api/v1/ \
  -t greenhouse-admin:latest \
  .
2

Run Container

Run the containerized application:
docker run -d \
  -p 80:80 \
  --name greenhouse-admin \
  greenhouse-admin:latest
The application will be available at http://localhost.
3

Health Check

The container includes a health check endpoint:
curl http://localhost/health
Should return OK.
The multi-stage build process:Stage 1 - Builder (gradle:8.10-jdk21):
  • Installs libatomic1 (required for Node.js v25+ used by Kotlin/Wasm)
  • Copies Gradle configuration and source code
  • Creates local.properties with API_BASE_URL from build argument
  • Upgrades Yarn lock files
  • Builds Wasm production distribution
Stage 2 - Runtime (nginx:alpine):
  • Copies custom Nginx configuration
  • Copies built Wasm application from builder stage
  • Exposes port 80 with health check endpoint
Full configuration: Dockerfile in project root

Environment Configuration

The application uses BuildKonfig to inject environment-specific configuration at compile time:
composeApp/build.gradle.kts
buildkonfig {
    packageName = "com.apptolast.greenhouse.admin"
    defaultConfigs {
        buildConfigField(STRING, "API_BASE_URL", 
            localProperties.getProperty("API_BASE_URL", ""))
    }
}
Access in code:
import com.apptolast.greenhouse.admin.BuildKonfig

val apiUrl = BuildKonfig.API_BASE_URL

Browser Compatibility

Wasm Target

Minimum Browser Versions:
  • Chrome/Edge 119+
  • Firefox 120+
  • Safari 17.4+
Wasm GC support is required. Older browsers will not work with the Wasm target.

JS Target

Minimum Browser Versions:
  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+
The JS target provides broader compatibility for older browsers.

Troubleshooting

Cause: Missing COOP/COEP headersSolution: Ensure your web server sends the required headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
See the nginx.conf file for a complete example.
Cause: Incorrect MIME type configurationSolution: Add the Wasm MIME type to your server:
types {
    application/wasm wasm;
}
Cause: Outdated lock files after dependency changesSolution: Upgrade Yarn lock files:
./gradlew kotlinWasmUpgradeYarnLock
./gradlew kotlinUpgradeYarnLock
Cause: Insufficient JVM heap for GradleSolution: Increase Gradle memory in gradle.properties:
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m

Next Steps

Android Platform

Build and deploy to Android devices

iOS Platform

Build and deploy to iOS devices

Desktop Platform

Build native desktop applications

Architecture

Learn about the app architecture

Build docs developers (and LLMs) love