diff --git a/AGENTS.md b/AGENTS.md index 796e9cc..7bee665 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ Before doing anything else: 2. Read `USER.md` — this is who you're helping 3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context 4. **If in MAIN SESSION** (direct chat with your human): Also read `MEMORY.md` +5. **On new session greeting**: Show todos from `notes/tasks/personal-tasks.md` Don't ask permission. Just do it. @@ -48,6 +49,12 @@ Capture what matters. Decisions, context, things to remember. Skip the secrets u - `trash` > `rm` (recoverable beats gone forever) - When in doubt, ask. +## Code Habits + +- **ALWAYS push your code** after making changes. Don't leave commits sitting locally. +- After any coding session: `git add -A && git commit -m "..." && git push` +- If you forget, Donovan will be annoyed. Don't forget. + ## External vs Internal **Safe to do freely:** diff --git a/HEARTBEAT.md b/HEARTBEAT.md index 5973eb7..dd6bf63 100644 --- a/HEARTBEAT.md +++ b/HEARTBEAT.md @@ -7,5 +7,23 @@ Check `gmail unread` for new messages in hammer7839283@gmail.com. If new mail from Donovan (forwarded), read and summarize or take action. Track last check in `memory/heartbeat-state.json`. +### Todo App (2-3x daily) +Check the todo app at https://api.todo.donovankelly.xyz for updates: +- Auth: sign in as hammer@donovankelly.xyz (service account, ID: 1HltUpL3R0qZkVxIu0oQ3UoqBCHuuMpV) + - Get Hammer API key or creds from Bitwarden +- Check for tasks assigned to Hammer (assigneeId = Hammer's user ID) +- Check for new tasks or recently updated tasks +- Check for tasks with upcoming due dates (next 48h) +- Report anything urgent to Donovan +Track last check in `memory/heartbeat-state.json`. + ### Skills (weekly) Check clawdhub for new useful skills. + +### Clawdbot Community (weekly) +Research what people are doing with Clawdbot: +- Search YouTube for Clawdbot videos/tutorials, get transcripts +- Search X/Twitter for Clawdbot posts and workflows +- Check Discord community for ideas +- Compile interesting workflow ideas and add to `notes/tasks/ideas.md` +- Summarize findings for Donovan diff --git a/TOOLS.md b/TOOLS.md index dfc6785..219e222 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -4,6 +4,12 @@ Skills define *how* tools work. This file is for *your* specifics — the stuff ## Rules +### 🔒 Never Use HTTP for External Communication +- All external communication MUST use HTTPS — no exceptions +- Never expose services over plain HTTP on public IPs +- If a service only supports HTTP internally, put it behind a TLS-terminating reverse proxy +- This applies to webhooks, APIs, and any cross-server communication + ### 🔐 Never Ask for Secrets in Chat - Don't ask Donovan to paste API keys, passwords, or credentials in messages - Instead: walk him through adding them to Bitwarden or `~/.clawdbot/.env` himself @@ -30,9 +36,16 @@ Things like: - Bitwarden CLI installed at `/home/clawdbot/.npm-global/bin/bw` - API credentials go in `~/.clawdbot/.env` (BW_CLIENTID, BW_CLIENTSECRET, BW_PASSWORD) - Bitwarden data: `~/.config/Bitwarden CLI/` +- **Always store credentials in the shared org vault:** + - Organization: `4e3ffbdb-0f8b-4f7a-a276-b0a30160e33f` (Hammer's Credentials) + - Collection: `320f9e42-607e-4180-8533-b0a30160e342` (Default collection) + - Set `organizationId` and `collectionIds` when creating items — never leave them in personal vault ### Infrastructure - Host: Hostinger VPS, Ubuntu +- VPS IP: 72.60.68.214 +- Domain: hammer.donovankelly.xyz (points to this VPS) +- Dokploy server: 191.101.0.153 (separate VPS, hosts queue app etc.) - User: clawdbot (sudo, needs password) ### Email diff --git a/donovan-portfolio b/donovan-portfolio new file mode 160000 index 0000000..e49a4a4 --- /dev/null +++ b/donovan-portfolio @@ -0,0 +1 @@ +Subproject commit e49a4a451234461dcb70e0a11b548986efb2da80 diff --git a/hammer-queue b/hammer-queue new file mode 160000 index 0000000..e874caf --- /dev/null +++ b/hammer-queue @@ -0,0 +1 @@ +Subproject commit e874cafbecb78e60d169b827574f4d8add01eb85 diff --git a/hook-proxy.log b/hook-proxy.log new file mode 100644 index 0000000..b0bab10 --- /dev/null +++ b/hook-proxy.log @@ -0,0 +1 @@ +Hook proxy listening on 0.0.0.0:18790 diff --git a/hook-proxy.ts b/hook-proxy.ts new file mode 100644 index 0000000..002b21c --- /dev/null +++ b/hook-proxy.ts @@ -0,0 +1,21 @@ +const server = Bun.serve({ + port: 18790, + hostname: "0.0.0.0", + async fetch(req) { + const url = new URL(req.url); + if (!url.pathname.startsWith("/hooks")) { + return new Response("Not Found", { status: 404 }); + } + const target = `http://127.0.0.1:18789${url.pathname}${url.search}`; + const resp = await fetch(target, { + method: req.method, + headers: req.headers, + body: req.method !== "GET" ? await req.text() : undefined, + }); + return new Response(resp.body, { + status: resp.status, + headers: resp.headers, + }); + }, +}); +console.log(`Hook proxy listening on 0.0.0.0:${server.port}`); diff --git a/memory/2026-01-28.md b/memory/2026-01-28.md new file mode 100644 index 0000000..faa8a0c --- /dev/null +++ b/memory/2026-01-28.md @@ -0,0 +1,137 @@ +# 2026-01-28 + +## Todo App Deployment & Features + +### Deployed to Dokploy +- **API** (`todo-app-v2`) deployed and running at `https://api.todo.donovankelly.xyz` + - Source: raw compose, builds from `https://git.infra.nkode.tech/hammer/todo-app.git` + - Compose ID: `e07fMO8TXcHI_SkKqBWrl` + - ENV: APP_URL=https://app.todo.donovankelly.xyz (was wrong before, pointed to API) + - Hammer API key: stored in Dokploy env as HAMMER_API_KEY +- **Frontend** (`todo-app-web`) deployed at `https://app.todo.donovankelly.xyz` + - Source: raw compose, builds from `https://git.infra.nkode.tech/hammer/todo-app-web.git` + - Compose ID: `ofMFmzQhEYK-3LfH5QmU_` +- **DB** (`todo-app-db`) Postgres running on Dokploy + - Postgres ID: `WrvHLAc1kaqsrpTaSGOG4` + - Not externally exposed (firewall blocks) +- Dokploy API: `https://app.dokploy.com`, auth via `X-API-Key` header, key in Bitwarden ("dokploy api key") +- Dokploy server: 191.101.0.153 (Hostzinger kvm2), server ID: QppaqcotG-I-6fUYCNolg +- Deploy pattern: `compose.deploy` via tRPC API, poll `deployment.allByCompose` for status + +### Fixed cross-subdomain auth +- BetterAuth cookies were scoped to `api.todo.donovankelly.xyz` only +- Added `crossSubDomainCookies` config with domain `.donovankelly.xyz` +- Added `https://app.todo.donovankelly.xyz` to trustedOrigins + +### Fixed project loading +- `getProject` API route had invalid `isArchived` filter on tasks table (column doesn't exist on tasks) +- Removed the bad filter, projects load correctly now + +### Fixed invite links +- APP_URL env was set to API domain, generating wrong invite URLs +- Updated to `https://app.todo.donovankelly.xyz` + +### User accounts +- Donovan's admin account: `donovan@donovankelly.xyz`, creds in BW shared vault "Todo App (donovankelly.xyz)" +- Hammer service account: `hammer@donovankelly.xyz`, ID: `1HltUpL3R0qZkVxIu0oQ3UoqBCHuuMpV`, role: service +- BetterAuth has `auth.api.setPassword({ body: { userId, newPassword } })` for password resets + +### Features implemented (frontend) +- **Unit tests:** 60 tests (API client, auth store, tasks store, utils) +- **Project creation:** Inline form in sidebar with color picker +- **Task assignment:** Assignee selector + avatar display on tasks +- **Kanban board:** Board view at `/project/:id/board` with section columns +- **Collapsible sidebar:** Toggle to icon-only mode, persists in localStorage +- **Task detail panel:** Right slide-over with editable fields (title, description, due date, priority, assignee, project) +- **Due date picker:** Fixed broken hidden input, now visible styled input with clear button +- **Completed tasks archive:** Collapsible "Completed" section at bottom of views + "Done" column in board +- **Project selector in task detail:** Move tasks between projects +- **Section management:** Add/rename/delete sections from board view +- **Admin: role selector on invites** — pick admin/user when inviting +- **Admin: change user roles** — dropdown in users table +- **Admin: password reset** — key icon to reset any user's password + +### Infrastructure notes +- Local Caddy container (`todo-caddy`) on this VPS is unnecessary — domains point to Dokploy +- Git remotes: `gitea` for todo-app, `origin` for todo-app-web + +### Network App Frontend +- Built SPA at `/home/clawdbot/clawd/network-app-web` (Vite + React + TS + Tailwind) +- Gitea: `hammer/network-app-web` +- Dokploy compose ID: `Sa1LrtH5uu-a7chrtebXb` +- Domain: `https://app.donovankelly.xyz` +- API at: `https://api.donovankelly.xyz` (compose ID: `UKrNvUyMCdaSWkl6DcAGA`) +- DB: `network-app-db` (Postgres, compose ID: `KzFkJETXrW_oMaiPsUb2o`) +- Features: Clients CRM, Events tracking, AI email generation, Profile/settings +- Pages: Dashboard, Clients list, Client detail, Events, Emails, Settings, Login + +### Bitwarden rule +- Donovan confirmed: ALWAYS store credentials in Hammer's Credentials org → Default collection +- Updated TOOLS.md with org/collection IDs + +### Lessons learned +- Always create BW items in shared org vault from the start +- Dokploy compose services: use `sourceType: raw` with git URL in build context (not `sourceType: git`) +- BetterAuth password hashing: don't try to manually hash with bcrypt — use `auth.api.setPassword()` or `auth.api.signUpEmail()` +- APP_URL must point to frontend, not API (used for invite link generation) + +### Hammer Queue — Task System +- **URL:** https://queue.donovankelly.xyz +- **API auth:** Bearer token from BW "Hammer Queue (donovankelly.xyz)" → API_BEARER_TOKEN +- **Creds saved to:** ~/.clawdbot/.env as HAMMER_QUEUE_URL + HAMMER_QUEUE_TOKEN +- **Purpose:** When Donovan asks me to do something, log it here immediately so it survives context compaction +- **Active tasks:** "update queue app" (detail panel, webhook integration) +- **High priority queued:** That2ndGuy pitch deck, Network App dev +- **Lesson:** ALWAYS write tasks to the queue when asked to do something. Don't rely on conversation memory. + +### Dokploy API Key Rotated +- Donovan added new key to BW "dokploy api key" (64 chars) +- Updated ~/.clawdbot/.env with DOKPLOY_API_KEY +- Old key was expiring/rotating — new one works + +### Hammer Queue — Continued Development (Late Session) + +#### Dokploy Access +- Created Dokploy account: hammer7839283@gmail.com, role: member +- Creds in BW: "Dokploy (app.dokploy.com)" in shared org vault +- Member role can't access compose services (authorization error) — need admin for full UI access +- Dokploy API has NO endpoint to read deployment build logs — logs only via UI WebSocket or SSH +- Learned: `deployment.allByCompose` works with API key auth for listing deployments + +#### Deploy Debugging Saga +- 7 consecutive deploy failures, root causes: + 1. **Unused `useState` import** in TaskCard.tsx — TypeScript strict mode (`tsc -b`) rejected it, Docker frontend build failed silently + 2. **`serial` column type** — `drizzle-kit push` can't add serial to existing table with data. Fixed by using `integer` + app-level sequencing + startup backfill +- **Key lesson:** ALWAYS run `bun run build` locally before deploying. A 2-second local build catches what 30 minutes of API spelunking can't. +- Created debugging skill at `skills/debugging/SKILL.md` — living document, update with every new lesson + +#### Features Shipped (HQ-15 completed) +- Sequential task IDs: HQ-1, HQ-2, etc. (integer column + backfill on startup) +- API resolves tasks by UUID, number, or HQ-N prefix +- GET /api/tasks/:id endpoint +- Editable task fields in detail panel: click-to-edit title/description, clickable priority/source selectors +- Webhook env vars (CLAWDBOT_HOOK_URL, CLAWDBOT_HOOK_TOKEN) deployed to Dokploy compose +- Compose file updated to pass webhook vars to backend service + +#### Queue App Infrastructure +- Compose ID: `kBdwrcZodIRyNIvQ-wrzG` +- Git remote: `origin` → `https://git.infra.nkode.tech/hammer/hammer-queue.git` +- Deploy pattern: push to Gitea → `compose.deploy` via Dokploy API → poll `deployment.allByCompose` +- Frontend: Vite + React + TS + Tailwind, builds with `bun run build` (tsc -b && vite build) +- Backend: Elysia + Drizzle + Postgres, starts with `bun run db:push && bun run start` + +#### Task Management Rules (from Donovan) +- When asked to do something → add to Hammer Queue immediately (survives context compaction) +- If task stalls or stops → mark it as such, move out of active +- Don't leave tasks "active" while doing something else +- Break large tasks into smaller ones when shipping partial work + +#### New Tasks Created +- HQ-16: Maintain Debugging Skill (living document) +- HQ-17: Queue App: Projects with Context (remaining from HQ-15) + +#### Hammer Queue Connection Info +- URL: https://queue.donovankelly.xyz +- API auth: Bearer token from BW "Hammer Queue (donovankelly.xyz)" → API_BEARER_TOKEN +- Creds in ~/.clawdbot/.env: HAMMER_QUEUE_URL, HAMMER_QUEUE_TOKEN +- Also has HAMMER_QUEUE_API_KEY (older, may be redundant) diff --git a/memory/2026-01-29.md b/memory/2026-01-29.md new file mode 100644 index 0000000..5cf5613 --- /dev/null +++ b/memory/2026-01-29.md @@ -0,0 +1,51 @@ +# 2026-01-29 + +## Lessons Learned +- **Don't update Donovan in chat about task progress** — put comments and updates directly in the task (HammerQueue or wherever the task lives). He doesn't want chat noise for status updates. +- **Never expose services publicly without asking first** — I enabled hammer.donovankelly.xyz which exposed the Clawdbot Control UI to anyone. Always ask before making something internet-facing. +- **Never use HTTP for external communication** — all cross-server comms must be HTTPS. Added to TOOLS.md rules. +- **Always build and test before pushing** — run `bun run build` (or equivalent) locally before pushing to git. Dokploy builds from git, so broken code = failed deploys. + +## Hammer Queue Webhook Fix Verified +- Cron job confirmed the webhook integration is working (3:59 AM UTC) + +## Hammer Queue → Hammer Dashboard +- Renamed domain from queue.donovankelly.xyz to dash.donovankelly.xyz via Dokploy API +- Dokploy is at app.dokploy.com (cloud), API key in Bitwarden +- Compose ID: kBdwrcZodIRyNIvQ-wrzG +- Fixed TS build errors (unused var, useEffect return type) before successful deploy +- hammer.donovankelly.xyz Caddy proxy DISABLED — exposed Clawdbot Control UI without auth. Must add auth layer before re-enabling. + +## Dashboard Improvements (Task Worker) - 6:00 AM UTC +- Added Dashboard overview page at `/` — stats grid, active task cards, up-next queue, recent activity feed, recently completed +- Added progress note input UI in TaskDetailPanel (textarea + Cmd+Enter + Add button) +- Added search bar and priority filter to Queue page +- Committed chat backend relay code (WebSocket proxy from dashboard to Clawdbot gateway) +- Gateway-relay.ts now falls back to VITE_WS_TOKEN env var +- Completed "Save/cancel button" task (was already implemented, just marked done) +- Chat still blocked: needs Caddy WSS proxy re-enabled on hammer.donovankelly.xyz (restricted to WS, not control UI) +- Three deploys to Dokploy + +## Task Worker: Due Dates, Subtasks, Task Detail Page - 7:00 AM UTC +- Added `due_date` and `subtasks` JSONB columns to tasks schema +- Backend: full CRUD for subtasks (add/toggle/delete at /api/tasks/:id/subtasks) +- Backend: dueDate support in create/update task endpoints +- TaskDetailPanel: due date picker with overdue/due-soon badges, subtask checklist with progress bar +- New TaskPage component: full-page task view at /task/HQ-{number} routes +- Dashboard cards now link to /task/HQ-{number}, show subtask progress bars and due date badges +- Frontend types updated with assigneeName, assigneeId, dueDate, subtasks +- Drizzle migration 0001_mighty_callisto.sql (uses db:push on deploy) +- Deployed to Dokploy +- Chat still blocked: needs Caddy (no sudo access to install) + +## Projects Feature (HQ-17) - 5:00 AM UTC +- Built full Projects feature for Hammer Dashboard +- Backend: `projects` table (name, description, context, repos, links), full CRUD API at `/api/projects` +- Backend: added `projectId` FK on tasks table, supported in create/update task APIs +- Frontend: Projects page with card grid, detail view, context editor, repo list, task assignment +- Frontend: Project selector added to TaskDetailPanel (dropdown in task detail) +- Sidebar nav now: Queue → Projects → Chat +- Created initial projects: "Hammer Dashboard" (with full context), "Network App (NWM CRM)" +- Assigned 7 dashboard tasks to Hammer Dashboard project, 1 task to Network App +- HQ-17 completed, HQ-21 updated with progress +- All deployed to dash.donovankelly.xyz via Dokploy diff --git a/memory/heartbeat-state.json b/memory/heartbeat-state.json index cc95a36..61fb961 100644 --- a/memory/heartbeat-state.json +++ b/memory/heartbeat-state.json @@ -1,6 +1,14 @@ { "lastChecks": { - "email": null, - "skills": null + "email": 1769660400, + "todoApp": 1738094400, + "hammerQueue": 1769660400, + "skills": 1769523930, + "community": null + }, + "notes": { + "skills": "clawdhub search timed out", + "todoApp": "API key in .env is placeholder 'hammer-todo-key'. Auth still fails.", + "hammerQueue": "Primary task system. Creds in .env. Tasks now have taskNumber (HQ-N). API resolves by UUID, number, or HQ-N. Cron task-worker runs every 30min. assigneeId/assigneeName fields now supported." } } diff --git a/network-app-api b/network-app-api new file mode 160000 index 0000000..11ee9b9 --- /dev/null +++ b/network-app-api @@ -0,0 +1 @@ +Subproject commit 11ee9b946f296a3cf6145d36ecb1a8b11e67d854 diff --git a/network-app-mobile b/network-app-mobile new file mode 160000 index 0000000..b191cfe --- /dev/null +++ b/network-app-mobile @@ -0,0 +1 @@ +Subproject commit b191cfe083285417d035cb02cb56bd967cdf495e diff --git a/network-app-web b/network-app-web new file mode 160000 index 0000000..b6de50b --- /dev/null +++ b/network-app-web @@ -0,0 +1 @@ +Subproject commit b6de50ba5e1874c5c2a9f70f6883cc2dfba8e38d diff --git a/notes b/notes index 4f14da9..a05cead 160000 --- a/notes +++ b/notes @@ -1 +1 @@ -Subproject commit 4f14da9136b5acff5d270a93191895bbb2c77fce +Subproject commit a05cead8ed1010921492b7a3f59b626b813cffca diff --git a/skills/app-builder.skill b/skills/app-builder.skill new file mode 100644 index 0000000..ccb4330 Binary files /dev/null and b/skills/app-builder.skill differ diff --git a/skills/app-builder/SKILL.md b/skills/app-builder/SKILL.md new file mode 100644 index 0000000..908bcd9 --- /dev/null +++ b/skills/app-builder/SKILL.md @@ -0,0 +1,132 @@ +--- +name: app-builder +description: Build and deploy web applications following a standardized process. Use when asked to create a new app, prototype, SaaS tool, or web project. Covers the full lifecycle from ideation through production deployment with a repeatable stack (React + Vite + Tailwind, Elysia + Bun, PostgreSQL + Drizzle, BetterAuth, Dokploy). Use for any request like "build me an app", "create a tool", "make a dashboard", or "prototype this idea". +--- + +# App Builder + +Standardized process for building and deploying web applications. Every app follows the same stack, structure, and lifecycle. + +## Stack + +| Layer | Tech | Why | +|-------|------|-----| +| Frontend | React + Vite + TypeScript + Tailwind | Fast, typed, utility CSS | +| Backend | Elysia + Bun + TypeScript | Fast, type-safe, great DX | +| Database | PostgreSQL + Drizzle ORM | Industry standard + lightweight ORM | +| Auth | BetterAuth (invite-only signup) | TypeScript-native, modern | +| AI (if needed) | LangChain.js | Model-agnostic, swap providers | +| Email (if needed) | Resend | Simple API, 3k free/month | +| Jobs (if needed) | pg-boss | Postgres-backed job queue | +| Deploy | Dokploy (self-hosted) | Full control, predictable costs | +| Git | Gitea (git.infra.nkode.tech) | Self-hosted, private | + +## Lifecycle Phases + +### Phase 1: Ideation +1. Gather requirements from the user (what, who, why) +2. Write a requirements doc at `notes/projects//requirements.md` +3. Research competitors if relevant +4. Get user approval on scope before proceeding + +### Phase 2: Scaffold +1. Create project directory at `/home/clawdbot/clawd/` +2. Use the template structure from `assets/template/` as a starting point +3. Create Gitea repos (one for monorepo, or separate frontend/backend) +4. Customize schema for the specific app +5. Ensure Hammer service account routes are included + +### Phase 3: Development +1. Build features incrementally +2. Write tests from day one +3. Use local docker-compose for development +4. Commit and push frequently +5. Update the task queue dashboard with progress + +### Phase 4: Test Deployment +1. Create Dokploy compose deployment +2. Deploy to test subdomain: `test-.donovankelly.xyz` +3. Set environment variables in Dokploy +4. Store all credentials in Bitwarden shared vault +5. Verify basic functionality + +### Phase 5: User Review +1. Notify user that test deployment is ready +2. User tests and provides feedback +3. Iterate on feedback (return to Phase 3 as needed) +4. Get explicit approval before going to production + +### Phase 6: Production Deployment +1. Create production Dokploy compose +2. Deploy to production subdomain: `.donovankelly.xyz` +3. Configure production environment variables +4. Verify all features work +5. Set up monitoring/health checks + +### Phase 7: Maintenance +- Feature requests go through test env first +- DB migrations via Drizzle (`bun run db:push` for dev, `bun run db:migrate` for prod) +- Keep staging synced with prod schema +- Rollback: Dokploy supports redeploying previous builds + +## Every App Must Have + +- [ ] **BetterAuth with `disableSignUp: true` (invite-only)** — NO EXCEPTIONS. Every app requires authentication. No public-facing pages without login. +- [ ] Hammer service account + `/api/hammer/*` routes (bearer token auth) +- [ ] All API routes behind auth (session or bearer token) +- [ ] Structured logging (console.log with JSON in prod) +- [ ] Unit tests (Vitest for frontend, bun:test for backend) +- [ ] Health check endpoint (`GET /api/health`) — only unauthenticated route allowed +- [ ] docker-compose.yml (local dev) +- [ ] docker-compose.dokploy.yml (production) +- [ ] `.env.example` with all required vars documented +- [ ] All secrets in Bitwarden shared vault + +**Auth is not optional.** Even internal tools, dashboards, and single-user apps must have login. If Donovan is the only user, he still logs in. No public read access to any data. + +## Environment Variable Naming + +Standard env vars across all apps: +``` +DATABASE_URL=postgresql://... +PORT=3001 +NODE_ENV=production +APP_URL=https://app. +ALLOWED_ORIGINS=https://app. +BETTER_AUTH_SECRET= +HAMMER_API_KEY= +RESEND_API_KEY= +FROM_EMAIL= +``` + +## Credential Management + +- All credentials go in Bitwarden shared vault (org: Hammer's Credentials) +- Create a Bitwarden entry per app: ` ()` +- Store HAMMER_API_KEY as a field on the app's BW entry +- Add HAMMER_API_KEY to `~/.clawdbot/.env` as `HAMMER__API_KEY` +- Never hardcode secrets, never echo them in chat + +## Dokploy Deployment + +See `references/deploy.md` for detailed Dokploy deployment steps. + +## Rollback Protocol + +See `references/rollback.md` for rollback and recovery procedures. + +## DB Migration Strategy + +See `references/migrations.md` for database migration best practices. + +## Legal & Payments + +See `references/legal-payments.md` for legal protection and payment processing options. + +## Project Documentation + +For every app, create in `notes/projects//`: +- `README.md` — overview, stack, status +- `requirements.md` — feature spec +- `architecture.md` — technical design (if complex) +- `feasibility.md` — assessment (if needed) diff --git a/skills/app-builder/assets/template/.env.example b/skills/app-builder/assets/template/.env.example new file mode 100644 index 0000000..1b41e46 --- /dev/null +++ b/skills/app-builder/assets/template/.env.example @@ -0,0 +1,20 @@ +# Database +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/APP_TEMPLATE + +# Server +PORT=3001 +NODE_ENV=development + +# URLs +APP_URL=http://localhost:5173 +ALLOWED_ORIGINS=http://localhost:5173 + +# Auth +BETTER_AUTH_SECRET=dev-secret-change-in-production + +# Hammer Service Account +HAMMER_API_KEY=hammer-dev-key-12345 + +# Email (optional) +# RESEND_API_KEY= +# FROM_EMAIL= diff --git a/skills/app-builder/assets/template/.gitignore b/skills/app-builder/assets/template/.gitignore new file mode 100644 index 0000000..4274b51 --- /dev/null +++ b/skills/app-builder/assets/template/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +*.log +.DS_Store diff --git a/skills/app-builder/assets/template/api/Dockerfile b/skills/app-builder/assets/template/api/Dockerfile new file mode 100644 index 0000000..e72c455 --- /dev/null +++ b/skills/app-builder/assets/template/api/Dockerfile @@ -0,0 +1,7 @@ +FROM oven/bun:1-alpine +WORKDIR /app +COPY package.json bun.lock* ./ +RUN bun install --frozen-lockfile 2>/dev/null || bun install +COPY . . +EXPOSE 3001 +CMD ["bun", "run", "src/index.ts"] diff --git a/skills/app-builder/assets/template/api/drizzle.config.ts b/skills/app-builder/assets/template/api/drizzle.config.ts new file mode 100644 index 0000000..9233f37 --- /dev/null +++ b/skills/app-builder/assets/template/api/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/skills/app-builder/assets/template/api/package.json b/skills/app-builder/assets/template/api/package.json new file mode 100644 index 0000000..43ec53d --- /dev/null +++ b/skills/app-builder/assets/template/api/package.json @@ -0,0 +1,23 @@ +{ + "name": "APP_TEMPLATE-api", + "version": "0.0.1", + "scripts": { + "dev": "bun run --watch src/index.ts", + "start": "bun run src/index.ts", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "test": "bun test" + }, + "dependencies": { + "better-auth": "^1.0.0", + "drizzle-orm": "^0.38.0", + "elysia": "^1.2.0", + "postgres": "^3.4.0" + }, + "devDependencies": { + "@types/bun": "latest", + "drizzle-kit": "^0.30.0" + } +} diff --git a/skills/app-builder/assets/template/api/src/db/index.ts b/skills/app-builder/assets/template/api/src/db/index.ts new file mode 100644 index 0000000..9b15e49 --- /dev/null +++ b/skills/app-builder/assets/template/api/src/db/index.ts @@ -0,0 +1,6 @@ +import { drizzle } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import * as schema from './schema'; + +const client = postgres(process.env.DATABASE_URL!); +export const db = drizzle(client, { schema }); diff --git a/skills/app-builder/assets/template/api/src/db/schema.ts b/skills/app-builder/assets/template/api/src/db/schema.ts new file mode 100644 index 0000000..59949ec --- /dev/null +++ b/skills/app-builder/assets/template/api/src/db/schema.ts @@ -0,0 +1,54 @@ +import { pgTable, text, timestamp, boolean, uuid } from 'drizzle-orm/pg-core'; + +// BetterAuth tables (managed by BetterAuth, do not modify) +export const users = pgTable('users', { + id: text('id').primaryKey(), + name: text('name').notNull(), + email: text('email').notNull().unique(), + emailVerified: boolean('email_verified').default(false), + image: text('image'), + role: text('role').default('user'), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +export const sessions = pgTable('sessions', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id), + token: text('token').notNull().unique(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +export const accounts = pgTable('accounts', { + id: text('id').primaryKey(), + userId: text('user_id').notNull().references(() => users.id), + accountId: text('account_id').notNull(), + providerId: text('provider_id').notNull(), + password: text('password'), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +export const verifications = pgTable('verifications', { + id: text('id').primaryKey(), + identifier: text('identifier').notNull(), + value: text('value').notNull(), + expiresAt: timestamp('expires_at').notNull(), + createdAt: timestamp('created_at').defaultNow(), + updatedAt: timestamp('updated_at').defaultNow(), +}); + +// ============================================ +// APP-SPECIFIC TABLES — Customize below +// ============================================ + +// Example: +// export const items = pgTable('items', { +// id: uuid('id').primaryKey().defaultRandom(), +// title: text('title').notNull(), +// userId: text('user_id').references(() => users.id), +// createdAt: timestamp('created_at').defaultNow(), +// updatedAt: timestamp('updated_at').defaultNow(), +// }); diff --git a/skills/app-builder/assets/template/api/src/index.ts b/skills/app-builder/assets/template/api/src/index.ts new file mode 100644 index 0000000..464a667 --- /dev/null +++ b/skills/app-builder/assets/template/api/src/index.ts @@ -0,0 +1,14 @@ +import { Elysia } from 'elysia'; +import { cors } from '@elysiajs/cors'; +import { hammerRoutes } from './routes/hammer'; + +const app = new Elysia() + .use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173'], + credentials: true, + })) + .get('/api/health', () => ({ status: 'ok', timestamp: new Date().toISOString() })) + .use(hammerRoutes) + .listen(Number(process.env.PORT) || 3001); + +console.log(`🚀 Server running on port ${app.server?.port}`); diff --git a/skills/app-builder/assets/template/api/src/lib/auth.ts b/skills/app-builder/assets/template/api/src/lib/auth.ts new file mode 100644 index 0000000..5e9481c --- /dev/null +++ b/skills/app-builder/assets/template/api/src/lib/auth.ts @@ -0,0 +1,17 @@ +import { betterAuth } from 'better-auth'; +import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { db } from '../db'; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { provider: 'pg' }), + emailAndPassword: { + enabled: true, + disableSignUp: true, // Invite-only + }, + trustedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173'], + advanced: { + crossSubDomainCookies: process.env.NODE_ENV === 'production' + ? { domain: process.env.COOKIE_DOMAIN || '' } + : undefined, + }, +}); diff --git a/skills/app-builder/assets/template/api/src/routes/hammer.ts b/skills/app-builder/assets/template/api/src/routes/hammer.ts new file mode 100644 index 0000000..7815566 --- /dev/null +++ b/skills/app-builder/assets/template/api/src/routes/hammer.ts @@ -0,0 +1,38 @@ +import { Elysia, t } from 'elysia'; +import { db } from '../db'; +import { users } from '../db/schema'; +import { eq } from 'drizzle-orm'; + +const validateHammerAuth = (authHeader: string | undefined): boolean => { + if (!authHeader) return false; + const token = authHeader.replace('Bearer ', ''); + return token === process.env.HAMMER_API_KEY; +}; + +export const hammerRoutes = new Elysia({ prefix: '/api/hammer' }) + .derive(({ request, set }) => { + const authHeader = request.headers.get('authorization'); + if (!validateHammerAuth(authHeader)) { + set.status = 401; + throw new Error('Invalid API key'); + } + return {}; + }) + + .get('/me', async ({ set }) => { + const hammerUser = await db.query.users.findFirst({ + where: eq(users.role, 'service'), + }); + if (!hammerUser) { + set.status = 404; + throw new Error('Hammer service account not found'); + } + return { + id: hammerUser.id, + name: hammerUser.name, + email: hammerUser.email, + role: hammerUser.role, + }; + }); + + // Add app-specific Hammer routes below diff --git a/skills/app-builder/assets/template/docker-compose.dokploy.yml b/skills/app-builder/assets/template/docker-compose.dokploy.yml new file mode 100644 index 0000000..6c5a7c5 --- /dev/null +++ b/skills/app-builder/assets/template/docker-compose.dokploy.yml @@ -0,0 +1,27 @@ +services: + api: + build: + context: ./api + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 3001 + environment: + - DATABASE_URL=${DATABASE_URL} + - PORT=3001 + - NODE_ENV=production + - APP_URL=${APP_URL} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - HAMMER_API_KEY=${HAMMER_API_KEY} + command: sh -c 'bun run db:push && bun run src/index.ts' + + web: + build: + context: ./web + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 80 + depends_on: + - api diff --git a/skills/app-builder/assets/template/docker-compose.yml b/skills/app-builder/assets/template/docker-compose.yml new file mode 100644 index 0000000..738f217 --- /dev/null +++ b/skills/app-builder/assets/template/docker-compose.yml @@ -0,0 +1,40 @@ +services: + db: + image: postgres:16-alpine + restart: unless-stopped + ports: + - 5432:5432 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: APP_TEMPLATE + volumes: + - pgdata:/var/lib/postgresql/data + + api: + build: + context: ./api + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 3001:3001 + env_file: .env + depends_on: + - db + volumes: + - ./api/src:/app/src + + web: + build: + context: ./web + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 5173:5173 + depends_on: + - api + volumes: + - ./web/src:/app/src + +volumes: + pgdata: diff --git a/skills/app-builder/assets/template/web/Dockerfile b/skills/app-builder/assets/template/web/Dockerfile new file mode 100644 index 0000000..bd574ee --- /dev/null +++ b/skills/app-builder/assets/template/web/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/skills/app-builder/assets/template/web/nginx.conf b/skills/app-builder/assets/template/web/nginx.conf new file mode 100644 index 0000000..6850d82 --- /dev/null +++ b/skills/app-builder/assets/template/web/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://api:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/skills/app-builder/assets/template/web/package.json b/skills/app-builder/assets/template/web/package.json new file mode 100644 index 0000000..1315ffb --- /dev/null +++ b/skills/app-builder/assets/template/web/package.json @@ -0,0 +1,28 @@ +{ + "name": "APP_TEMPLATE-web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "vitest" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vitest": "^2.0.0" + } +} diff --git a/skills/app-builder/references/deploy.md b/skills/app-builder/references/deploy.md new file mode 100644 index 0000000..de05394 --- /dev/null +++ b/skills/app-builder/references/deploy.md @@ -0,0 +1,66 @@ +# Dokploy Deployment Guide + +## Prerequisites +- Dokploy server at 191.101.0.153 (Hostinger KVM2) +- Dokploy API key in Bitwarden ("dokploy api key") +- Gitea repos created at git.infra.nkode.tech + +## Compose File Structure + +Every app uses `docker-compose.dokploy.yml`: + +```yaml +services: + api: + build: + context: ./apps/api # or ./api, ./backend + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 3001 + environment: + - DATABASE_URL=${DATABASE_URL} + - PORT=3001 + - NODE_ENV=production + - APP_URL=${APP_URL} + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - HAMMER_API_KEY=${HAMMER_API_KEY} + command: sh -c 'bun run db:push && bun run src/index.ts' + + web: + build: + context: ./apps/web # or ./web, ./frontend + dockerfile: Dockerfile + restart: unless-stopped + ports: + - 80 + depends_on: + - api +``` + +## Deployment Steps + +1. Push code to Gitea +2. Create compose in Dokploy: + - Source type: raw compose with git URL + - Set env vars in Dokploy UI +3. Configure domains in Dokploy: + - API: `api..donovankelly.xyz` → api service port + - Web: `app..donovankelly.xyz` → web service port +4. Enable HTTPS (Dokploy handles Let's Encrypt) +5. Deploy and verify health check + +## Domain Pattern + +- Test: `test-.donovankelly.xyz` +- Production: `.donovankelly.xyz` +- API: `api..donovankelly.xyz` (or `api.todo.donovankelly.xyz`) +- Frontend: `app..donovankelly.xyz` (or `app.todo.donovankelly.xyz`) + +## Environment Variables + +Set in Dokploy compose env (not in docker-compose file): +- All `${VAR}` references resolve from Dokploy env settings +- Generate secrets with `openssl rand -hex 32` +- Store everything in Bitwarden immediately after creating diff --git a/skills/app-builder/references/legal-payments.md b/skills/app-builder/references/legal-payments.md new file mode 100644 index 0000000..9a96939 --- /dev/null +++ b/skills/app-builder/references/legal-payments.md @@ -0,0 +1,47 @@ +# Legal Protection & Payments + +## Legal (Lean Approach) + +### What Pieter Levels Uses +Pieter Levels (maker of Nomad List, Remote OK, Photo AI) keeps it minimal: +- Simple Terms of Service page +- Simple Privacy Policy page +- Generated with free/cheap tools, not expensive services like Termly + +### Recommended Approach +1. **Terms of Service** — Use a free generator (TermsFeed free tier, GetTerms.io) or write a simple one +2. **Privacy Policy** — Required if collecting any user data. Free generators available +3. **Cookie Banner** — Only needed if using analytics/tracking cookies +4. **Business Entity** — LLC ($50-150 depending on state) for liability protection +5. **Don't over-engineer** — Until you have paying users, simple legal pages are fine + +### When to Upgrade +- Taking payments → need proper ToS with refund policy +- Handling health data → HIPAA considerations +- EU users → GDPR compliance (data export, deletion rights) +- Enterprise clients → may need SOC 2, BAA agreements + +## Payments + +### Options (Easiest to Hardest) + +| Service | Fees | Best For | Setup Time | +|---------|------|----------|------------| +| Lemon Squeezy | 5% + $0.50 | Merchant of record, handles tax/VAT | 1 day | +| Paddle | 5% + $0.50 | Same as Lemon Squeezy, more established | 1 day | +| Stripe | 2.9% + $0.30 | Full control, most flexible | 2-3 days | +| Gumroad | 10% | Digital products, simplest | Hours | + +### Recommendation +- **Start with Lemon Squeezy or Paddle** — they handle sales tax, VAT, and act as merchant of record (you don't need a business entity) +- **Move to Stripe** when you need more control or lower fees at scale +- Both have simple JS SDKs and webhook integrations + +### Integration Pattern +``` +User clicks "Subscribe" → Redirect to payment provider checkout +→ Provider handles payment → Webhook to your API +→ API updates user subscription status in DB +``` + +Keep payment logic out of your app. Let the provider handle checkout, invoicing, and tax. diff --git a/skills/app-builder/references/migrations.md b/skills/app-builder/references/migrations.md new file mode 100644 index 0000000..461b396 --- /dev/null +++ b/skills/app-builder/references/migrations.md @@ -0,0 +1,49 @@ +# Database Migration Strategy + +## Drizzle ORM Migrations + +### Development (Local / Test) +Use `db:push` for rapid iteration: +```bash +bun run db:push +``` +This syncs schema directly — fast but destructive. Fine for dev/test. + +### Production +Use `db:migrate` with generated migration files: +```bash +bun run db:generate # creates SQL migration file +bun run db:migrate # applies migration +``` + +### Workflow +1. Change schema in `src/db/schema.ts` +2. Run `bun run db:generate` — creates migration in `drizzle/` +3. Review the generated SQL +4. Write rollback SQL in `drizzle/rollback/` (same filename) +5. Test migration on staging +6. Apply to production + +## Safe Migration Practices + +### Adding columns +- Always add as nullable or with a default value +- Never add non-nullable columns without defaults to tables with existing data + +### Removing columns +- First deploy: stop reading/writing the column in code +- Second deploy: remove the column from schema +- Two-phase approach prevents errors during rolling deploys + +### Renaming columns +- Don't rename directly — add new column, migrate data, remove old column + +### Adding indexes +- Use `CREATE INDEX CONCURRENTLY` for large tables (avoids locks) +- Drizzle may not generate concurrent indexes — check generated SQL + +## Backup Before Migration +Always backup before production migrations: +```bash +pg_dump -Fc $DATABASE_URL > backup-$(date +%Y%m%d-%H%M%S).dump +``` diff --git a/skills/app-builder/references/rollback.md b/skills/app-builder/references/rollback.md new file mode 100644 index 0000000..ffbb10b --- /dev/null +++ b/skills/app-builder/references/rollback.md @@ -0,0 +1,43 @@ +# Rollback & Recovery + +## Levels of Rollback + +### 1. Code Rollback (Most Common) +- Dokploy keeps previous builds +- Redeploy previous compose version from Dokploy UI +- Or: `git revert` the breaking commit, push, redeploy + +### 2. Database Rollback +- Drizzle doesn't auto-generate down migrations +- For schema changes, write explicit rollback SQL before deploying +- Keep a `migrations/rollback/` directory with undo scripts +- For data issues: restore from Dokploy Postgres backup + +### 3. Full Rollback +- Dokploy allows complete service redeployment from any previous state +- Database: restore from backup +- Last resort: rebuild from Gitea source at known-good commit + +## Pre-Deploy Checklist + +Before any production deployment: +- [ ] Feature works in test environment +- [ ] User has approved in test +- [ ] DB migration tested (if schema changed) +- [ ] Rollback SQL written (if schema changed) +- [ ] Health check passes after deploy + +## Staging Environment + +- Staging should mirror production schema +- Periodically sync staging DB schema with prod +- Never sync prod DATA to staging (privacy) +- Test migrations on staging before prod + +## If Something Breaks in Prod + +1. **Assess severity** — is the app down or is it a bug? +2. **If app is down** — redeploy previous Dokploy build immediately +3. **If it's a bug** — fix in dev, test, deploy fix +4. **If DB is corrupted** — restore from backup, investigate cause +5. **Notify user** with what happened and what was done diff --git a/skills/app-builder/scripts/scaffold.sh b/skills/app-builder/scripts/scaffold.sh new file mode 100755 index 0000000..1c0c758 --- /dev/null +++ b/skills/app-builder/scripts/scaffold.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# Scaffold a new app from the standard template +# Usage: scaffold.sh [--api-only] +# +# Creates project structure, initializes git, and pushes to Gitea. + +set -euo pipefail + +APP_NAME="${1:?Usage: scaffold.sh }" +API_ONLY="${2:-}" +BASE_DIR="/home/clawdbot/clawd" +PROJECT_DIR="$BASE_DIR/$APP_NAME" +SKILL_DIR="$(dirname "$(realpath "$0")")/.." +TEMPLATE_DIR="$SKILL_DIR/assets/template" +GITEA_URL="https://git.infra.nkode.tech" + +if [ -d "$PROJECT_DIR" ]; then + echo "Error: $PROJECT_DIR already exists" + exit 1 +fi + +echo "🔨 Scaffolding $APP_NAME..." + +# Create project structure +mkdir -p "$PROJECT_DIR" +cp -r "$TEMPLATE_DIR/api" "$PROJECT_DIR/api" +if [ "$API_ONLY" != "--api-only" ]; then + cp -r "$TEMPLATE_DIR/web" "$PROJECT_DIR/web" +fi + +# Copy root files +cp "$TEMPLATE_DIR/docker-compose.yml" "$PROJECT_DIR/" +cp "$TEMPLATE_DIR/docker-compose.dokploy.yml" "$PROJECT_DIR/" +cp "$TEMPLATE_DIR/.env.example" "$PROJECT_DIR/" +cp "$TEMPLATE_DIR/.gitignore" "$PROJECT_DIR/" + +# Replace placeholder app name +find "$PROJECT_DIR" -type f \( -name "*.ts" -o -name "*.json" -o -name "*.yml" -o -name "*.md" -o -name "*.env*" \) \ + -exec sed -i "s/APP_TEMPLATE/$APP_NAME/g" {} + + +# Initialize git +cd "$PROJECT_DIR" +git init +git add -A +git commit -m "Initial scaffold from app-builder template" + +echo "" +echo "✅ Project scaffolded at $PROJECT_DIR" +echo "" +echo "Next steps:" +echo " 1. Create Gitea repo: $GITEA_URL/hammer/$APP_NAME" +echo " 2. git remote add origin $GITEA_URL/hammer/$APP_NAME.git" +echo " 3. git push -u origin main" +echo " 4. Customize the schema in api/src/db/schema.ts" +echo " 5. Run: cd $PROJECT_DIR && docker compose up" diff --git a/skills/debugging/SKILL.md b/skills/debugging/SKILL.md new file mode 100644 index 0000000..bb4011f --- /dev/null +++ b/skills/debugging/SKILL.md @@ -0,0 +1,68 @@ +# Debugging Skill + +Hard-won lessons from real failures. Follow this before spending time on dead ends. + +## Rule 1: Build Locally First +Before deploying to any remote environment (Dokploy, CI, etc.), **always verify the build locally**: +- `bun run build` / `npm run build` — catches TypeScript errors, unused imports, missing modules +- `docker build .` — catches Dockerfile issues, dependency problems +- Run the test suite if one exists + +**Why:** A TypeScript strict mode error (`TS6133: unused import`) caused 7 consecutive Dokploy deploy failures. A 2-second local build would have caught it instantly. Instead, 30 minutes were wasted trying to read remote logs that weren't accessible. + +*Learned: 2026-01-29 — hammer-queue deploy failures* + +## Rule 2: Reproduce Before Escalating +When a remote deploy/service fails: +1. **Build locally first** (Rule 1) +2. **Run locally** if possible (`bun run dev`, `docker compose up`) +3. **Check the runtime** — does the app start? Does `db:push`/migration work? +4. Only after local reproduction fails should you investigate server-side issues + +**Why:** The `serial` column type worked in schema definition but broke `drizzle-kit push` on an existing table with data. Building locally passed, but running locally would have caught the db migration failure. + +*Learned: 2026-01-29 — serial column broke db:push on existing table* + +## Rule 3: Check the Obvious First +Before diving into API spelunking or log hunting: +- Compiler errors? (`tsc`, build output) +- Missing imports or unused imports? (strict mode) +- Schema changes compatible with existing data? +- Environment variables set correctly? +- Ports/URLs correct? + +## Rule 4: When Blind to Logs, Create Your Own +If you can't access remote logs (no API endpoint, no SSH, no UI access): +- **Don't** spend more than 5 minutes hunting for log endpoints +- **Do** reproduce the issue locally where you CAN see logs +- **Do** add health check endpoints that report startup errors +- **Do** add structured error logging that surfaces in API responses + +## Rule 5: Time-Box Dead Ends +If an approach isn't working after 5-10 minutes, switch strategies: +- Can't read Dokploy logs via API? → Build locally instead +- Can't SSH to server? → Use available APIs differently +- API returning opaque errors? → Test the component in isolation + +## Rule 6: Schema Migration Safety +When modifying database schemas on existing tables: +- `serial` columns can't be safely added to existing tables via `drizzle-kit push` — use `integer` with app-level sequencing instead +- Always consider: "What happens to existing rows?" +- Test migrations against a database with real data, not just empty tables +- Prefer nullable columns with backfill logic over NOT NULL additions + +## Deployment Checklist +Before every deploy: +``` +[ ] Local build passes (frontend + backend) +[ ] TypeScript strict mode clean (no unused imports/vars) +[ ] Schema changes tested against existing data +[ ] Environment variables verified +[ ] Push to git +[ ] Deploy +[ ] Verify health endpoint after deploy +[ ] Verify API functionality +``` + +--- +*This skill is a living document. Update it every time a new debugging lesson is learned.* diff --git a/todo-app b/todo-app new file mode 160000 index 0000000..617eaac --- /dev/null +++ b/todo-app @@ -0,0 +1 @@ +Subproject commit 617eaacc5fffa83c1fb25bf06b8e825bc5315b3d diff --git a/todo-app-web b/todo-app-web new file mode 160000 index 0000000..6f26e11 --- /dev/null +++ b/todo-app-web @@ -0,0 +1 @@ +Subproject commit 6f26e1117c85a51a53f983fcdce839062bc7f7b5