Skip to main content
pack-settings running inside the khal-os shell
A frontend-only pack is the simplest shape a Khal pack can take. No service/ directory, no Docker image, no Helm chart — just a React component published as an npm package that the shell loads when your pack is installed. pack-settings is the reference implementation for this pattern.

When to pick this shape

Use frontend-only when

  • The pack needs no long-running backend process.
  • All data comes from the platform (the current user, org-scoped APIs the shell already exposes) or from third-party APIs called straight from the browser.
  • You need only one window, with no scheduled or background work.

Go full-stack when

  • The pack owns stateful data you don’t want to trust to the browser.
  • You need to hold a persistent process (PTY, daemon, subscription).
  • You need server-side secrets that must never touch the frontend.
Full-stack pack pattern

Directory shape

pack-settings/
├── khal-app.json         # Manifest — no services[] entry
├── package/
│   ├── src/index.tsx     # Default export: a React component
│   ├── tsup.config.ts    # CJS + ESM + .d.ts build
│   └── package.json      # Publishes @khal-os/pack-settings
├── package.json          # Workspace root
└── .github/workflows/    # CI + npm publish
No service/. No helm/. No Dockerfile. The pack lives entirely in the browser.

Manifest

{
  "$schema": "https://raw.githubusercontent.com/khal-os/app-kit/dev/packages/types/src/khal-app-schema.json",
  "id": "settings",
  "name": "Settings",
  "version": "1.0.0",
  "icon": "./package/src/assets/icon.svg",
  "description": "User preferences and workspace settings",
  "author": "KhalOS Core Team",
  "permissions": ["user:read", "user:update"],
  "frontend": {
    "package": "@khal-os/pack-settings"
  }
}
The backend and services keys are simply absent. The platform infers “frontend-only” from their absence.

Typical SDK usage

Frontend-only packs reach for one hook: useKhalAuth() — exposes the current userId, orgId, role, permissions, and a loading flag. React state handles everything else. Reach for useNats only when you want to broadcast user-visible events to other packs (e.g., “theme changed”).
import { useKhalAuth } from '@khal-os/sdk/app';

export default function SettingsPack() {
  const auth = useKhalAuth();

  if (!auth || auth.loading) {
    return <p>Loading…</p>;
  }

  return (
    <div>
      <h2>Signed in as {auth.userId}</h2>
      <p>Org: {auth.orgId}</p>
      <p>Role: {auth.role}</p>
    </div>
  );
}
The SDK does not expose a signOut method — sign-out is a desktop-shell concern, not a pack concern. The user signs out of KhalOS itself, not out of an individual pack. See useKhalAuth for the full contract.

Publishing

1

Push to dev

CI publishes @khal-os/pack-settings@next to GitHub Packages.
2

Merge to main

CI publishes @latest. No Docker step; no Helm step.
3

Install into an instance

A customer admin adds @khal-os/pack-settings@latest through their platform’s install flow. The shell loads the npm package on next launch.
Frontend-only packs ship faster than full-stack ones because there is no container image to build and no Helm chart to package. Keep the pack frontend-only unless you need server-side behavior.

What’s next

Full-stack pack pattern

When you need a backend, go here.

Publish your pack

The full publish pipeline (npm → Docker → Helm).