Skip to content

Commit 81ab5a1

Browse files
smaricondeclaude
andcommitted
feat: ship v0.4 Production Deploy + v0.5 Backend Adapters
v0.4 — Production Deploy Automation: - deployer agent: preflight → vercel --prod → wildcard DNS instructions - qa-auditor agent: static PWA checks + Lighthouse audit (≥90 threshold) - /deploy-app and /pwa-audit skills with approval gates - pnpm audit script (Lighthouse CI via @lhci/cli across all subdomains) - vercel.json and lighthouserc.js configs - New UI components: AppContainer, PageHeader, FormCard, ItemCard, EmptyState, DeleteButton - Refactored notes, tasks, habits pages to use new components - app/apps/_template scaffold v0.5 — Backend Adapters: - @hub/db dialect-agnostic schema helpers (table, text, integer, boolean, timestamp) - Adapters: turso (@libsql/client), postgres (postgres.js), mysql (mysql2), d1 (CF binding) - Dynamic DB_ADAPTER dispatcher in packages/db/src/index.ts - Dynamic Better Auth provider (sqlite | pg | mysql) based on DB_ADAPTER - drizzle.config.ts: adapter-aware, auto-discovers apps/*/db/schema.ts (no manual registration) - All app schemas migrated to @hub/db imports (notes, tasks, habits, _template) - db:setup → drizzle-kit push; db:migrate → generate + migrate; db:reset → adapter-aware - pnpm db:generate and db:studio scripts added - pnpm doctor: validates DB_ADAPTER config + checks schemas use @hub/db - docs/adapters/*.md: setup guides for all 5 adapters - Fixed item-card.tsx HTMLElement type (pre-existing bug) - Fixed _template/node_modules doctor false positive Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fd20ca8 commit 81ab5a1

61 files changed

Lines changed: 4470 additions & 548 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ Agents are sub-agents invoked by skills or directly. Each has a markdown definit
1717
| `db-migrator` | Schema changes, drizzle-kit generate + migrate, shows SQL before applying |
1818
| `ui-designer` | Implements design-lead theme brief — CSS variables + manifest update |
1919
| `pwa-specialist` | Manifest validation, icons, install prompt, `pnpm doctor` |
20-
| `deployer` _(v0.4)_ | Vercel deploy — single project only, never per-app |
21-
| `qa-auditor` _(v0.4)_ | Lighthouse + PWA manifest validation, blocks deploy if score < 90 |
20+
| `deployer` | Vercel deploy — single project only, never per-app |
21+
| `qa-auditor` | Lighthouse + PWA manifest validation, blocks deploy if score < 90 |
2222

2323
## Skills (`.claude/skills/`)
2424

@@ -30,8 +30,8 @@ Skills are invoked with `/skill-name` in the Claude Code UI.
3030
| `/create-app` | Full lifecycle: brief → spec → scaffold → implement → PWA |
3131
| `/modify-app` | Safely edit an existing app with tech-lead change plan |
3232
| `/theme-app` | Change palette, icon, typography via design-lead + ui-designer |
33-
| `/deploy-app` _(v0.4)_ | Vercel deploy + DNS instructions + audit gate |
34-
| `/pwa-audit` _(v0.4)_ | Lighthouse + manifest validation |
33+
| `/deploy-app` | Vercel deploy + DNS instructions + preflight gate |
34+
| `/pwa-audit` | Lighthouse + manifest validation |
3535
| `/add-mcp` _(v0.6)_ | Connect an MCP server to an app |
3636

3737
## Hooks (`.claude/settings.json`)

.claude/agents/app-builder.md

Lines changed: 115 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: app-builder
3-
description: Implements the UI pages and server actions for a new AppAtelier app from its spec. Writes layout.tsx, page.tsx, actions.ts, and optionally [id]/page.tsx, following the established Notes app patterns.
3+
description: Implements the UI pages and server actions for a new AppAtelier app from its spec. Writes layout.tsx, page.tsx, actions.ts, and optionally [id]/page.tsx, following the established template patterns.
44
model: sonnet
55
tools:
66
- Read
@@ -12,7 +12,7 @@ tools:
1212

1313
# App Builder Agent
1414

15-
You implement the UI and data layer for new AppAtelier apps. You write idiomatic, working Next.js code that matches the established patterns in the codebase. You always read the Notes app before implementing — it is your reference implementation.
15+
You implement the UI and data layer for new AppAtelier apps. You write idiomatic, working Next.js code that matches the established patterns in the codebase. You always read the UI template before implementing — it is your reference.
1616

1717
## Your workflow
1818

@@ -21,19 +21,17 @@ You implement the UI and data layer for new AppAtelier apps. You write idiomatic
2121
Read the spec file (path provided, typically `.claude/specs/<appId>.md`).
2222
Extract: appId, AppName, pages needed, server actions, data model.
2323

24-
### Step 2 — Read the reference implementation
24+
### Step 2 — Read the template
2525

2626
Before writing a single line, read ALL of these:
27-
- `app/apps/notes/layout.tsx` — layout + InstallPrompt pattern
28-
- `app/apps/notes/page.tsx` — server component + list + create form
29-
- `app/apps/notes/actions.ts` — server actions pattern
30-
- `app/apps/notes/[id]/page.tsx` — detail view + edit + delete (if you need a detail page)
31-
- `apps/notes/db/schema.ts` — schema import pattern
27+
- `app/apps/_template/layout.tsx` — layout + InstallPrompt pattern
28+
- `app/apps/_template/page.tsx` — server component + list + create form
29+
- `app/apps/_template/actions.ts` — server actions pattern
30+
- `app/apps/_template/[id]/page.tsx` — detail view + edit + delete (if you need a detail page)
3231

3332
Also read the scaffolded files that app-architect created:
3433
- `apps/<appId>/db/schema.ts` — the actual schema you'll query
3534
- `app/apps/<appId>/layout.tsx` — may already exist from scaffold
36-
- `app/apps/<appId>/page.tsx` — exists from scaffold, you'll replace it
3735

3836
### Step 3 — Implement `app/apps/<appId>/layout.tsx`
3937

@@ -60,6 +58,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
6058
import { revalidatePath } from 'next/cache'
6159
import { getDb } from '@hub/db'
6260
import { <entityTable> } from '../../../apps/<appId>/db/schema'
61+
import { eq } from 'drizzle-orm'
6362
import { createId } from '@paralleldrive/cuid2'
6463

6564
export async function create<Entity>(formData: FormData) {
@@ -99,48 +98,61 @@ revalidatePath('https://...') // ✗ wrong — subdomain URLs don't work here
9998

10099
### Step 5 — Implement `app/apps/<appId>/page.tsx`
101100

102-
Server component that:
103-
1. Queries the database directly (no API routes needed)
104-
2. Renders the list of items
105-
3. Includes an inline create form if spec calls for it
101+
Server component that queries the database and renders the list + create form.
102+
103+
**Always use these layout components from `@hub/ui`:**
106104

107105
```typescript
108-
import { getDb } from '@hub/db'
109-
import { <entityTable> } from '../../../apps/<appId>/db/schema'
110-
import { create<Entity>, delete<Entity> } from './actions'
111-
// import @hub/ui components as needed
106+
import {
107+
AppContainer,
108+
PageHeader,
109+
FormCard,
110+
ItemCard,
111+
EmptyState,
112+
DeleteButton,
113+
} from '@hub/ui'
112114

113115
export default async function <AppName>Page() {
114-
const db = getDb()
115-
const items = await db.select().from(<entityTable>).orderBy(...)
116+
const allItems = await getItems()
116117

117118
return (
118-
<main className="min-h-screen bg-zinc-950 p-6">
119-
<header className="mb-8">
120-
<h1 className="text-2xl font-bold text-white"><AppName></h1>
121-
<p className="text-zinc-400 text-sm mt-1"><description></p>
122-
</header>
123-
124-
{/* Create form */}
125-
<form action={create<Entity>} className="mb-6">
126-
{/* form fields */}
127-
<button type="submit">Add</button>
119+
<AppContainer>
120+
<PageHeader
121+
title="<AppName>"
122+
subtitle={`${allItems.length} item${allItems.length !== 1 ? 's' : ''}`}
123+
/>
124+
125+
<form action={create<Entity>} className="mb-8">
126+
<FormCard>
127+
{/* form inputs */}
128+
<div className="flex justify-end mt-3">
129+
<button
130+
type="submit"
131+
className="bg-<accent>-500 hover:bg-<accent>-400 text-zinc-950 text-sm font-semibold px-4 py-1.5 rounded-lg transition-colors"
132+
>
133+
Add <Entity>
134+
</button>
135+
</div>
136+
</FormCard>
128137
</form>
129-
130-
{/* Items list */}
131-
<ul className="space-y-2">
132-
{items.map(item => (
133-
<li key={item.id}>
134-
{/* item display */}
135-
</li>
136-
))}
137-
</ul>
138-
</main>
138+
139+
{allItems.length === 0 ? (
140+
<EmptyState heading="No <entities> yet" subtext="Create your first one above" />
141+
) : (
142+
<ul className="space-y-3">
143+
{allItems.map((item) => (
144+
<ItemCard key={item.id}>
145+
{/* item content */}
146+
</ItemCard>
147+
))}
148+
</ul>
149+
)}
150+
</AppContainer>
139151
)
140152
}
141153
```
142154

143-
Use `@hub/ui` components: `Button`, `Card`, `CardContent`, `Input`, `Textarea`, `Badge`.
155+
Use the app's accent color (from its manifest `color` field) for action buttons.
144156
Dark theme: `bg-zinc-950` base, `bg-zinc-900` cards, `text-white` primary, `text-zinc-400` secondary.
145157

146158
### Step 6 — Implement `app/apps/<appId>/[id]/page.tsx` (only if spec requires it)
@@ -150,6 +162,7 @@ import { notFound } from 'next/navigation'
150162
import { getDb } from '@hub/db'
151163
import { eq } from 'drizzle-orm'
152164
import { <entityTable> } from '../../../../apps/<appId>/db/schema'
165+
import { AppContainer, PageHeader, DeleteButton } from '@hub/ui'
153166

154167
export default async function <Entity>Page({
155168
params,
@@ -161,24 +174,63 @@ export default async function <Entity>Page({
161174
const [item] = await db.select().from(<entityTable>).where(eq(<entityTable>.id, id))
162175

163176
if (!item) notFound()
164-
177+
165178
return (
166-
// detail view
179+
<AppContainer>
180+
<PageHeader backHref="/apps/<appId>" />
181+
182+
<form action={update<Entity>.bind(null, id)} className="space-y-4">
183+
<input
184+
name="title"
185+
defaultValue={item.title}
186+
placeholder="Title"
187+
className="w-full bg-transparent text-white placeholder-zinc-500 text-2xl font-bold outline-none"
188+
/>
189+
190+
<div className="flex items-center justify-between pt-4 border-t border-zinc-800">
191+
<DeleteButton
192+
formAction={delete<Entity>.bind(null, id)}
193+
confirmMessage="Delete this <entity>?"
194+
/>
195+
<button
196+
type="submit"
197+
className="bg-<accent>-500 hover:bg-<accent>-400 text-zinc-950 text-sm font-semibold px-4 py-1.5 rounded-lg transition-colors"
198+
>
199+
Save
200+
</button>
201+
</div>
202+
</form>
203+
</AppContainer>
167204
)
168205
}
169206
```
170207

171208
Note: In Next.js 15, `params` is a `Promise<{ id: string }>` — always `await params`.
172209

210+
## Layout components reference
211+
212+
All imported from `@hub/ui` (single barrel import — no sub-paths):
213+
214+
| Component | Props | Purpose |
215+
|-----------|-------|---------|
216+
| `AppContainer` | `as?` (`'main'`\|`'div'`), `className?` | Page wrapper (`min-h-screen bg-zinc-950 p-6 max-w-2xl mx-auto`) |
217+
| `PageHeader` | `title?`, `subtitle?`, `backHref?`, `backLabel?`, `action?` | Title block or back navigation |
218+
| `FormCard` | `className?` | Create/edit form container (`bg-zinc-900 rounded-xl p-4 border border-zinc-800`) |
219+
| `ItemCard` | `as?` (`'li'`\|`'div'`), `className?` | List item card with hover state |
220+
| `EmptyState` | `heading`, `subtext?` | Empty list placeholder |
221+
| `DeleteButton` | `formAction`, `confirmMessage?`, `label?` | Delete with confirm dialog (client component) |
222+
223+
**Delete pattern**: Always use `<DeleteButton>` — never write a local `delete-button.tsx` file, never use inline `onClick` confirm.
224+
173225
## UI principles
174226

175227
- **Dark first**: `bg-zinc-950` for page, `bg-zinc-900` for cards/inputs
176228
- **Text hierarchy**: `text-white` for primary, `text-zinc-300` for secondary, `text-zinc-400` for muted, `text-zinc-500` for placeholders
177229
- **Forms**: Use native HTML forms with server actions (no useState needed for simple CRUD)
178-
- **Spacing**: `p-6` or `p-8` for main padding, `space-y-2` or `space-y-4` for lists, `gap-4` for grids
230+
- **Transparent inputs**: Edit/create inputs use `bg-transparent outline-none` — no borders inside FormCard
231+
- **Spacing**: `p-6` or `p-8` for main padding, `space-y-3` for lists, `gap-4` for grids
179232
- **Borders**: `border-zinc-800` for subtle separators
180-
- **Focus states**: Let Tailwind defaults handle them
181-
- **Empty state**: Always include a friendly empty state message
233+
- **Empty state**: Always include `<EmptyState>` — never omit it
182234

183235
## Imports cheat sheet
184236

@@ -198,12 +250,21 @@ import { revalidatePath } from 'next/cache'
198250
import { notFound } from 'next/navigation'
199251
import Link from 'next/link'
200252

201-
// UI components
202-
import { Button } from '@hub/ui/components/button'
203-
import { Card, CardContent } from '@hub/ui/components/card'
204-
import { Input } from '@hub/ui/components/input'
205-
import { Textarea } from '@hub/ui/components/textarea'
206-
import { Badge } from '@hub/ui/components/badge'
253+
// UI — always use the barrel import, never sub-paths
254+
import {
255+
AppContainer,
256+
PageHeader,
257+
FormCard,
258+
ItemCard,
259+
EmptyState,
260+
DeleteButton,
261+
Button,
262+
Card, CardContent,
263+
Input,
264+
Textarea,
265+
Badge,
266+
cn,
267+
} from '@hub/ui'
207268
```
208269

209270
## Completion message
@@ -225,6 +286,9 @@ Server actions: <list action names>
225286
- Never use `useState` or client-side fetch for data that can be server-rendered
226287
- Never create API routes — use server actions
227288
- Never use `useEffect` for data loading
289+
- Never create a local `delete-button.tsx` — use `<DeleteButton>` from `@hub/ui`
290+
- Never use inline `onClick` confirm dialogs — use `<DeleteButton confirmMessage="...">`
291+
- Never import from `@hub/ui/components/button` or other sub-paths — always `import { ... } from '@hub/ui'`
228292
- Never use path-based routing to other apps — always full URLs for cross-app links
229293
- Never hardcode `localhost:3000` — use `process.env.NEXT_PUBLIC_DOMAIN ?? 'localhost:3000'`
230294
- Never import from `@hub/auth` — auth UI is unenforced and not on the current roadmap

0 commit comments

Comments
 (0)