Skip to content

Frontend Apps

Scaffold a frontend with your framework CLI under apps/, and kernstack deploy builds it and publishes it to a bucket. On deploy, the CLI runs build, copies out into the bucket seed, and uploads it. It then writes kernstack-routes.json (route URLs plus the core origin) into the bucket — fetch it at runtime instead of baking URLs into the build. Field reference: IaC — Frontend apps.

Wire-up

Every static frontend needs three pieces in stack.json:

  1. apps[] — build command, out, and seed path
  2. resources.buckets — bucket that receives the seed upload
  3. gateway.routes[] — "type": "prefix", "default_key": "index.html", target.kind: "bucket"
{
  "apps": [
    {
      "name": "web",
      "build": "npm ci --prefix apps/web && npm run build --prefix apps/web",
      "out": "apps/web/www",
      "seed": "seed/buckets/www"
    }
  ],
  "gateway": {
    "routes": [
      {
        "name": "www",
        "type": "prefix",
        "default_key": "index.html",
        "cdn_enabled": true,
        "target": { "kind": "bucket", "name": "www" }
      }
    ]
  },
  "resources": {
    "buckets": [{ "name": "www" }]
  }
}
  • Use "type": "prefix": a bucket route without it serves a single object, so / and assets 404.
  • Set seed when the app name (web) differs from the bucket (www); the default is seed/buckets/{app-name}.
  • Don't commit build outputs (www/, dist/, node_modules/); the scaffolded .gitignore covers them.
  • Redeploy one frontend without a stack apply: kernstack deploy --only app:web (add --purge-cdn to purge only that bucket's CDN routes). The bucket and routes must already exist from a full deploy.
  • Per environment, override apps[].build in a profile overlay.
  • Upload files with presigned URLs returned by a function; the browser PUTs straight to the bucket. Resolve presigned /v1/buckets/… paths against the core origin in kernstack-routes.json, not the function URL.

Framework examples

Scaffold into apps/web and set out to the framework's build directory.

Static SPA with @sveltejs/adapter-static (fallback index.html). kernstack init --demo vendors this layout under apps/web. To hand-wire the same stack:

npm create svelte@latest apps/web
cd apps/web
npm install
npm install -D @sveltejs/adapter-static

Point the adapter at build.

{
  "apps": [
    {
      "name": "web",
      "build": "npm ci --prefix apps/web && npm run build --prefix apps/web",
      "out": "apps/web/build",
      "seed": "seed/buckets/www"
    }
  ]
}

Blank starter with Capacitor (web + iOS + Android). Kernstack publishes the web build only; native shells stay local (npx cap sync, Xcode / Android Studio).

mkdir -p apps
cd apps
ionic start web blank --type=angular-standalone --capacitor --no-interactive
cd web
npx cap add ios
npx cap add android

Confirm capacitor.config.ts has webDir: 'www' and the Angular build outputPath is www.

{
  "apps": [
    {
      "name": "web",
      "build": "npm ci --prefix apps/web && npm run build --prefix apps/web",
      "out": "apps/web/www",
      "seed": "seed/buckets/www"
    }
  ]
}

Local native workflow after a web build: npx cap sync then npx cap open ios / npx cap open android.

mkdir -p apps
cd apps
ng new web --directory=web --routing --style=css --ssr=false

Set projects.web.architect.build.options.outputPath to www (or keep dist/web/browser and point out at that path).

{
  "apps": [
    {
      "name": "web",
      "build": "npm ci --prefix apps/web && npm run build --prefix apps/web -- --configuration=production",
      "out": "apps/web/www",
      "seed": "seed/buckets/www"
    }
  ]
}
npm create vite@latest apps/web -- --template react-ts
# or: vue-ts | svelte-ts | vanilla-ts

Default output is dist.

{
  "apps": [
    {
      "name": "web",
      "build": "npm ci --prefix apps/web && npm run build --prefix apps/web",
      "out": "apps/web/dist",
      "seed": "seed/buckets/www"
    }
  ]
}

Client-side routing works out of the box: on a prefix route with an HTML default_key, unknown extensionless (or .html) paths are answered with default_key and HTTP 200, so deep links load the app. Missing assets (.js, .css, images) still return 404. Uploading errors/404.html switches the route to real 404s and turns this fallback off — see Not-found pages.

Static export only. SSR / Node servers belong on a worker, not apps[].

In next.config:

/** @type {import('next').NextConfig} */
const nextConfig = { output: 'export' }
module.exports = nextConfig
npx create-next-app@latest apps/web --ts --eslint --app --src-dir --no-tailwind --import-alias "@/*" --use-npm
{
  "apps": [
    {
      "name": "web",
      "build": "npm ci --prefix apps/web && npm run build --prefix apps/web",
      "out": "apps/web/out",
      "seed": "seed/buckets/www"
    }
  ]
}

No framework and no apps[]. Put files under seed/buckets/www/ and declare the bucket + prefix route only.

seed/buckets/www/index.html
seed/buckets/www/assets/...
{
  "gateway": {
    "routes": [
      {
        "name": "www",
        "type": "prefix",
        "default_key": "index.html",
        "cdn_enabled": true,
        "target": { "kind": "bucket", "name": "www" }
      }
    ]
  },
  "resources": {
    "buckets": [{ "name": "www" }]
  }
}

Native apps and SSR

  • Native shells (ios/, android/) stay under apps/ without an apps[] entry; only declared entries build.
  • For SSR (Next.js server mode, Nuxt, Remix), run the app as a worker with "src": "apps/web".