Apps

Create from scratch

End-to-end guide to building an OWD app module, from scaffold to desktop integration.

This walkthrough builds an app like app-about — a singleton “About” window registered via defineDesktopApp. Use it as a checklist when starting a new repository.

1. Scaffold the repository

Create a Nuxt module package with the layout from Package layout:

@owdproject/my-app/
├── package.json
├── src/
│   ├── module.ts
│   └── runtime/
│       ├── plugin.ts
│       ├── app.config.ts
│       └── components/Window/WindowMain.vue
└── playground/
    ├── nuxt.config.ts
    ├── desktop.config.ts
    └── package.json

package.json essentials:

  • "type": "module"
  • exports./dist/module.mjs
  • peerDependencies: "@owdproject/core": "^3.4.0"
  • devDependencies: nuxt, @nuxt/module-builder, @nuxt/schema
  • Scripts: dev:prepare (stub + playground prepare), prepack (release build)

Clone app-about and rename if you prefer starting from a working tree.

2. Write src/module.ts

The module wires components, the register plugin, and Tailwind scanning:

import {
  defineNuxtModule,
  createResolver,
  addComponentsDir,
  addPlugin,
} from '@nuxt/kit'
import { registerTailwindPath } from '@owdproject/kit-tailwind/kit/registerTailwindPath'

export default defineNuxtModule({
  meta: {
    name: 'owd-app-my-app',
    configKey: 'myApp', // optional — see step 3
  },
  async setup(_options, nuxt) {
    const { resolve } = createResolver(import.meta.url)

    addComponentsDir({ path: resolve('./runtime/components') })
    addPlugin(resolve('./runtime/plugin'))
    registerTailwindPath(nuxt, resolve('./runtime/components/**/*.{vue,mjs,ts}'))
  },
})

Optional configKey lets consumers override app-specific defaults from desktop.config.ts (About uses configKey: 'about' and merges into runtimeConfig.public.desktop.about).

3. Define runtime/app.config.ts

Export an ApplicationConfig object. From app-about:

export default {
  id: 'org.owdproject.about',
  title: 'About',
  category: 'system-tools',
  singleton: true,
  icon: 'mdi:hexagon-multiple-outline',
  windows: {
    main: {
      component: () => import('./components/Window/WindowAbout.vue'),
      resizable: false,
      size: { width: 448, height: 240 },
      position: { x: 400, y: 240, z: 0 },
    },
  },
  entries: {
    about: { command: 'about' },
  },
  commands: {
    about: (app) => {
      const existing = app.getFirstWindowByModel('main')
      if (existing) {
        existing.actions.setActive(true)
        existing.actions.bringToFront()
        return existing
      }
      return app.openWindow('main')
    },
  },
}

Details: Windows and commands.

4. Add runtime/plugin.ts

The register plugin must run client-side only and use a stable name for dependsOn ordering:

import { defineNuxtPlugin } from 'nuxt/app'
import { defineDesktopApp } from '@owdproject/core'
import config from './app.config'

export default defineNuxtPlugin({
  name: 'desktop-app-about-register',
  async setup() {
    if (import.meta.server) return
    await defineDesktopApp(config)
  },
})

Do not wrap defineDesktopApp in app:created — the current validator and reference apps call it directly in the plugin setup. See Plugins.

5. Build the window component

Window components live under runtime/components/Window/. Keep them theme-neutral:

  • Use DesktopWindowContent and core composables where possible.
  • Avoid importing theme-specific chrome (taskbar, Win95 borders, etc.).

About’s window is a simple panel with props read from runtimeConfig.public.desktop.about when using configKey.

6. Configure the playground

playground/nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['@owdproject/core'],
  ssr: false,
  compatibilityDate: 'latest',
})

playground/desktop.config.ts:

import { defineDesktopConfig } from '@owdproject/core'

export default defineDesktopConfig({
  theme: '@owdproject/theme-nova',
  apps: ['@owdproject/app-about'],
  modules: [],
})

Critical rule: every package listed in theme, apps, or modules must appear in playground/package.json dependencies. Mismatch causes install or runtime resolution errors.

playground/package.json (minimal):

{
  "dependencies": {
    "@owdproject/core": "^3.4.0",
    "@owdproject/theme-nova": "^3.4.0",
    "@owdproject/app-about": "^3.4.0",
    "nuxt": "^4.4.4"
  }
}

When the theme imports PrimeVue directly (Nova explorer), also add primevue and @primeuix/themes — see Playground and Create a theme.

cd playground && pnpm install && pnpm dev

Or from the app root: pnpm run dev (runs dev:prepare then playground dev).

7. Optional: playground launch plugin

For dev ergonomics and GitHub Pages, auto-open your app after boot. Create playground/app/plugins/launch-about.client.ts:

import { defineNuxtPlugin } from 'nuxt/app'

export default defineNuxtPlugin({
  name: 'app-about-playground-launch',
  dependsOn: ['desktop-app-about-register'],
  setup(nuxtApp) {
    autoStartPlaygroundApps(nuxtApp, [
      { id: 'org.owdproject.about', entry: 'about', windowModel: 'main' },
    ])
  },
})

Do not guard with if (!import.meta.dev) return — static generate and GitHub Pages need the same path.

Full reference: app-about launch plugin. See Plugins.

8. Validate and wire into the desktop

From the app repo root:

desktop validate .

Checks include src/runtime/plugin.ts, app.config.ts, playground deps on your package and core, and optional launch plugin.

To test inside the client monorepo:

// desktop/package.json
"@owdproject/app-about": "workspace:*"
// desktop/desktop.config.ts
apps: ['@owdproject/app-about']
pnpm install && pnpm run dev

Next