Two ways to create a new app: the /create-app AI skill (recommended), or manually. Both end up in the same place.
Type /create-app in Claude Code. The skill guides you through a brief, produces a spec for your approval, scaffolds the app, and implements the UI. See docs/ai-studio.md for the full walkthrough.
pnpm new-app <name>Where <name> is lowercase letters and hyphens only (e.g., tasks, habit-tracker). This copies apps/_template/ to apps/<name>/, replacing __APP_ID__ and __APP_NAME__ placeholders throughout.
After scaffolding, the script prints two manual wiring steps you must complete.
app/manifest.ts — add an import and a registry entry so the PWA manifest endpoint returns the right manifest for your app's subdomain:
import tasksApp from '../apps/tasks/manifest'
const registry: Record<string, AppManifest> = {
notes: notesApp,
tasks: tasksApp, // ← add this
}app/apps/hub/page.tsx — add an import and an entry in the apps array so the hub shows your app's icon:
import tasksApp from '../../../../apps/tasks/manifest'
const apps = [notesApp, tasksApp] // ← add tasksAppEdit apps/<name>/manifest.ts to describe your app:
import { defineManifest } from '@hub/core'
export default defineManifest({
id: 'tasks',
subdomain: 'tasks',
name: 'Tasks',
description: 'Simple personal task list',
icon: 'check-square', // Lucide icon name (kebab-case)
color: '#6366F1', // hex — used for icon background in hub
enabled: true,
status: 'stable',
pwa: {
themeColor: '#6366F1',
backgroundColor: '#09090B',
display: 'standalone',
icons: [
{ src: '/tasks-icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/tasks-icon-512.png', sizes: '512x512', type: 'image/png' },
{ src: '/tasks-icon-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
],
},
database: {
schemaPath: './db/schema.ts',
tablePrefix: 'tasks_', // REQUIRED — all tables must start with this
},
})Edit apps/<name>/db/schema.ts. Every table name must start with tablePrefix:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { createId } from '@paralleldrive/cuid2'
export const tasks = sqliteTable('tasks_tasks', { // ← prefix: tasks_
id: text('id').primaryKey().$defaultFn(() => createId()),
title: text('title').notNull().default(''),
done: integer('done', { mode: 'boolean' }).default(false),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
})Then push the schema to the database:
pnpm db:setupdrizzle-kit auto-discovers schemas by globbing apps/*/db/schema.ts — no registration needed.
Create app/apps/<name>/:
app/apps/tasks/
layout.tsx ← import InstallPrompt, set metadata
page.tsx ← main view (list + create form)
actions.ts ← server actions (createTask, updateTask, deleteTask)
[id]/
page.tsx ← detail/edit view (optional)
Use app/apps/notes/ as a reference implementation.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string |
yes | Unique identifier. Lowercase, hyphens allowed. Matches the directory name. |
subdomain |
string |
yes | The subdomain. Usually identical to id. |
name |
string |
yes | Display name shown in the hub icon and browser tab. |
description |
string |
no | Short tagline (under 60 chars recommended). Used in the PWA manifest. |
version |
string |
no | Semver string. Informational only. |
icon |
string |
yes | Lucide icon name in kebab-case (e.g., notebook-pen, check-square). |
color |
string |
yes | Hex color for the icon background in the hub (e.g., #F59E0B). |
enabled |
boolean |
yes | false hides the app from the hub without removing it. |
status |
'stable' | 'beta' | 'experimental' |
yes | Shown as a badge in the hub on non-stable apps. |
pwa.themeColor |
string |
yes | Browser chrome color. Usually matches color. |
pwa.backgroundColor |
string |
yes | Splash screen background. Use #09090B for dark. |
pwa.display |
'standalone' | 'fullscreen' | 'minimal-ui' | 'browser' |
yes | Use standalone for a native-feeling app. |
pwa.orientation |
'portrait' | 'landscape' | 'any' |
no | Lock orientation. Omit for unrestricted. |
pwa.icons |
Array<{src, sizes, type?, purpose?}> |
yes | At minimum: 192px, 512px, and a 512px maskable icon. |
database.schemaPath |
string |
if DB used | Relative path to the Drizzle schema file. |
database.tablePrefix |
string |
if DB used | Required. All table names must start with this (e.g., tasks_). |
database.migrations |
string |
no | Path to migrations directory. Optional; push mode works without it. |
aiContext.description |
string |
no | Plain-language description for AI agents. |
aiContext.domain |
string |
no | Category (e.g., productivity, health, finance). |
aiContext.examples |
string[] |
no | Example user utterances for AI context. |
mcpServers |
string[] |
no | MCP server IDs the app uses. Known values: gmail, google-calendar, notion, drive. |
Every Drizzle table must be named <tablePrefix><tablename>. The prefix prevents collisions when multiple apps share the same SQLite file (or Postgres schema).
Example with tablePrefix: 'tasks_':
tasks_tasks— the main tasks tabletasks_tags— a tags tabletasks_task_tags— a join table
Never name a table without the prefix. pnpm doctor will warn if it detects a schema violation.
After creating an app, generate PWA icons from a 1024×1024 PNG:
pnpm generate-icons --input ./my-icon.png --app tasksOutput files written to public/:
tasks-icon-192.pngtasks-icon-512.pngtasks-icon-maskable.png(icon centered at 80% with padding, on zinc-950 background)
pnpm doctor # validates manifests, schemas, PWA config
pnpm typecheck # type-check the whole monorepo
pnpm dev # start the dev serverOpen http://<name>.localhost:3000 — you should see your app. Open http://localhost:3000 — you should see your app's icon in the hub.
Type /delete-app <appId> in Claude Code. The skill shows a full summary of what will be removed, asks for confirmation, then calls the script. Safe by default — nothing is deleted until you type "yes".
pnpm delete-app <name>The script removes all artifacts for the given app and verifies with pnpm doctor at the end. Run with --yes to skip the interactive prompt (useful in scripts):
pnpm delete-app finance --yesWhat gets deleted:
| Artifact | Location |
|---|---|
| Workspace package | apps/<name>/ |
| Next.js routes | app/apps/<name>/ |
| PWA icons | public/<name>-icon-*.png |
| Manifest registry entry | app/manifest.ts |
| Hub launcher entry | app/apps/hub/page.tsx |
What is NOT deleted automatically:
Database tables are left intact. If you want to drop them, run pnpm db:reset — this drops and recreates tables for all remaining apps. There is no per-app table drop command by design, to avoid accidental data loss.
To undo a deletion (before committing):
git restore apps/<name>/ app/apps/<name>/
# PWA icons are not tracked by git — regenerate with:
pnpm generate-icons --app <name> --input <1024px.png>