| name | new-deno-app |
| description | Scaffold a new Deno 2 + Hono + Deno KV + Eta + HTMX app with password auth and PWA support. Use when the user wants to create a new personal web app following this stack. |
| disable-model-invocation | true |
You are scaffolding a new personal web app. These apps follow a strict set of conventions:
- Deno 2 runtime, no build step — TypeScript runs natively
- Hono for routing (from JSR:
jsr:@hono/hono@^4)
- Deno KV for the database (built-in to Deno)
- Eta for server-side HTML templates (from JSR:
jsr:@eta-dev/eta@^3)
- HTMX 2 for frontend interactivity (CDN, no npm)
- PicoCSS classless for styling (CDN, no npm)
- Single shared
APP_PASSWORD env var for auth — SHA-256 hash in a session cookie
- PWA:
manifest.json + minimal pass-through service worker
- Deployed to Deno Deploy via GitHub push to
main
Step 1: Gather information
Always ask the user for all of the following interactively — do not attempt to parse $ARGUMENTS:
- app-name — kebab-case, used for directory name and GitHub repo name
- Display Name — full title shown in browser tab and manifest (e.g. "Book Tracker")
- Short name — ≤12 characters, shown on Android home screen under the icon (e.g. "Books")
- Description — one sentence describing what the app does
- Theme color — hex color for PWA theme/background (default:
#ffffff)
- Emoji — single emoji used as the app logo in the header (default:
📋)
- Data model — what are the main things this app stores? Describe the entity name(s) and their fields briefly.
Confirm all values with the user before proceeding.
Step 2: Determine project directory
Check whether the current working directory is empty (contains no files or subdirectories, ignoring dotfiles like .git).
- If the current directory is empty: use it as the project root. All files will be created here.
- If the current directory is not empty: tell the user: "The current directory isn't empty. Please create and switch to a new empty directory for the project, then run
/new-deno-app again." Stop here — do not proceed.
Step 3: Create project
Create all files in the project root determined in Step 2. All files below use the exact patterns from the reference app.
deno.json
{
"imports": {
"hono": "jsr:@hono/hono@^4",
"hono/cookie": "jsr:@hono/hono@^4/cookie",
"hono/deno": "jsr:@hono/hono@^4/deno",
"@eta-dev/eta": "jsr:@eta-dev/eta@^3",
"@std/assert": "jsr:@std/assert@^1"
},
"tasks": {
"dev": "deno run --watch --allow-net --allow-env --allow-read --unstable-kv main.ts",
"start": "deno run --allow-net --allow-env --allow-read --unstable-kv main.ts",
"test": "DENO_TLS_CA_STORE=system deno test --allow-env --unstable-kv",
"fmt": "deno fmt",
"lint": "deno lint",
"check": "deno fmt --check && deno lint && deno check main.ts src/**/*.ts"
},
src/auth.ts
This is the exact auth pattern. Copy it verbatim — only the dev default password can be changed.
import type { Context, Next } from "hono"
import { getCookie, setCookie } from "hono/cookie"
async function hashPassword(password: string): Promise<string> {
const data = new TextEncoder().encode(password)
const buf = await crypto.subtle.digest("SHA-256", data)
return Array.from(new Uint8Array(buf))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
}
export function getSessionToken(): Promise<string> {
return hashPassword(Deno.env.get("APP_PASSWORD") ?? "changeme")
}
(): {
expected = ..() ??
submitted === expected
}
(): <> {
token = ()
isProd = ..() !==
(c, , token, {
: ,
: ,
: isProd,
: ,
: * * * ,
})
}
() {
(c.. === ) {
()
}
session = (c, )
expected = ()
(session !== expected) {
(c..()) {
c.(, )
c.(, )
}
c.()
}
()
}
src/db.ts
Generate this based on the user's described data model. The KV singleton must store a Promise<Deno.Kv> (not the resolved instance) to prevent races when concurrent calls arrive before the first open resolves. Always export closeKv() for tests.
Template — replace Item / ["items", id] with the actual entities:
import type { Item } from "./types.ts"
let _kv: Promise<Deno.Kv> | null = null
function kv(): Promise<Deno.Kv> {
if (!_kv) _kv = Deno.openKv()
return _kv
}
export async function getItem(id: string): Promise<Item | null> {
const db = await kv()
return (await db.get<Item>(["items", id])).value
}
export async function listItems(): Promise<Item[]> {
const db = await kv()
const items: Item[] = []
for await ( entry db.<>({ : [] })) {
items.(entry.)
}
items.( a..(b.))
}
(): <> {
db = ()
db.([, item.], item)
}
(): <> {
db = ()
db.([, id])
}
(): {
_kv?.( db.())
_kv =
}
src/types.ts
Define TypeScript interfaces based on the user's data model. Every entity must have:
id: string — use crypto.randomUUID() when creating
createdAt: string — ISO date string
src/views/eta.ts
import { Eta } from "@eta-dev/eta"
const isProd = Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined
export const deploymentId = Deno.env.get("DENO_DEPLOY_BUILD_ID") ?? "dev"
const templatesDir = new URL("../templates", import.meta.url).pathname
export const eta = new Eta({
views: templatesDir,
cache: isProd,
})
src/templates/layout.eta
Fill in Display Name, emoji, short_name, and theme color. Keep all PWA meta tags.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= it.title %> – <Display Name></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css">
<link rel="stylesheet" href="/static/styles.css">
<link rel="manifest" href="/static/manifest.json">
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="icon" href="/static/favicon.ico">
<meta name="theme-color" content="<theme-color>">
<%~ it.body %>
Build: <%= it.deploymentId %>
If the app has multiple sections, add nav links inside <ul> in the <nav>. Use it.active to mark the current section with class="active".
src/templates/login.eta
Fill in emoji and Display Name.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><Display Name></title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.classless.min.css">
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<main>
<div class="login-wrap">
<h1><emoji></h1>
<h2><Display Name></h2>
<p>Enter the password to continue.
Enter
<% if (it.error) { %>
Incorrect password. Try again.
<% } %>
src/templates/index.eta
Generate a sensible main page template for the app's primary entity. Use HTMX attributes (hx-get, hx-post, hx-delete, hx-target, hx-swap) for dynamic interactions. Keep it simple — a list view and a form to add new items.
static/manifest.json
{
"name": "<Display Name>",
"short_name": "<Short name>",
"start_url": "/",
"display": "standalone",
"background_color": "<theme-color>",
"theme_color": "<theme-color>",
"icons": [
{ "src": "/static/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/static/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
static/sw.js
self.addEventListener("fetch", (e) => e.respondWith(fetch(e.request)))
static/styles.css
.login-wrap {
max-width: 360px;
margin: 4rem auto;
text-align: center;
}
.login-error {
color: var(--pico-color-red-500);
}
Add any additional app-specific styles here. Keep it minimal — PicoCSS handles most things.
static/app.js
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/static/sw.js")
}
Add any additional client-side JS below the service worker registration.
main.ts
Wire up the Hono router. Follow this exact pattern for route organization:
import { Hono } from "hono"
import { setCookie } from "hono/cookie"
import { serveStatic } from "hono/deno"
import { authMiddleware, setSessionCookie, verifyPassword } from "./src/auth.ts"
import { deploymentId, eta } from "./src/views/eta.ts"
const app = new Hono()
app.use("/static/*", serveStatic({ root: "./" }))
app.get("/login", async (c) => {
const error = c.req.query("error")
return c.html(await eta.renderAsync("login", { error: !!error }))
})
app.post("/login", async (c) => {
const body = await c.req.formData()
const password = body.()?.() ??
(!(password)) {
c.()
}
(c)
c.()
})
app.(, c.())
app.(, {
(c, , , { : , : })
c.()
})
app.(, authMiddleware)
app.(, (c) => {
body = eta.(, { })
c.(
eta.(, { : , : , body, deploymentId }),
)
})
.(app.)
For HTMX partial responses: check c.req.header("HX-Request") and return only the fragment HTML, not the full layout.
.gitignore
.env
*.db
CLAUDE.md
Generate a CLAUDE.md tailored to this app using this template:
# CLAUDE.md
## Commands
\`\`\`bash
deno task dev # Start dev server with file watching
deno task test # Run all tests
deno task fmt # Format TypeScript files
deno task lint # Lint TypeScript files
deno task check # fmt check + lint + type check (run before committing)
\`\`\`
Formatting rules (from \`deno.json\`): 100-char line width, no semicolons, no single quotes enforced.
The \`no-explicit-any\` lint rule is disabled.
## Testing
Test files live alongside source files as \`src/*_test.ts\`. Run with \`deno task test\`.
**SSL certificates:** \`DENO_TLS_CA_STORE=system\` is set automatically by \`deno task test\` so Deno
uses the OS cert store for JSR imports.
**KV in tests:** \`src/db.ts\` exports \`closeKv()\` — call it in \`afterEach\` to release the KV
handle so Deno's leak sanitizer stays happy.
## Deployment
Pushing to \`main\` on GitHub automatically deploys to Deno Deploy.
## Architecture
**Stack:** Deno 2 + Hono + Deno KV + Eta + HTMX 2 + PicoCSS. No build step.
**Entry point:** \`main.ts\` — Hono router and all route handlers.
**Auth:** Single \`APP_PASSWORD\` env var. SHA-256 hash stored in session cookie. Dev default: \`changeme\`.
### KV Schema
\`\`\`
<describe the KV keys for each entity>
\`\`\`
Step 4: Create placeholder icon files
Create empty placeholder files for:
static/icon-192.png
static/icon-512.png
static/apple-touch-icon.png
static/favicon.ico
Tell the user: "You'll need to replace the placeholder icons in static/ with real ones before installing as a PWA."
Step 5: Initialize git and create GitHub repo
Run these commands in order:
git init
git add .
git commit -m "Initial scaffold"
gh repo create <app-name> --private --source=. --remote=origin --push
Step 6: Final instructions
Tell the user:
- Run locally:
deno task dev — opens on http://localhost:8000
- Dev password:
changeme (set APP_PASSWORD env var for production)
- Deploy to Deno Deploy: Go to dash.deno.com → "New Project" → connect the GitHub repo
<app-name>. Set the APP_PASSWORD env var in project settings.
- Replace icons in
static/ with real PNG/ICO files at the correct sizes.
- Data model is in
src/types.ts and src/db.ts — adjust as needed.