memory: log task worker session - due dates, subtasks, task page
This commit is contained in:
132
skills/app-builder/SKILL.md
Normal file
132
skills/app-builder/SKILL.md
Normal file
@@ -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/<app-name>/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/<app-name>`
|
||||
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-<app>.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: `<app>.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.<domain>
|
||||
ALLOWED_ORIGINS=https://app.<domain>
|
||||
BETTER_AUTH_SECRET=<random>
|
||||
HAMMER_API_KEY=<random>
|
||||
RESEND_API_KEY=<if needed>
|
||||
FROM_EMAIL=<if needed>
|
||||
```
|
||||
|
||||
## Credential Management
|
||||
|
||||
- All credentials go in Bitwarden shared vault (org: Hammer's Credentials)
|
||||
- Create a Bitwarden entry per app: `<App Name> (<domain>)`
|
||||
- Store HAMMER_API_KEY as a field on the app's BW entry
|
||||
- Add HAMMER_API_KEY to `~/.clawdbot/.env` as `HAMMER_<APP>_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/<app-name>/`:
|
||||
- `README.md` — overview, stack, status
|
||||
- `requirements.md` — feature spec
|
||||
- `architecture.md` — technical design (if complex)
|
||||
- `feasibility.md` — assessment (if needed)
|
||||
20
skills/app-builder/assets/template/.env.example
Normal file
20
skills/app-builder/assets/template/.env.example
Normal file
@@ -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=
|
||||
5
skills/app-builder/assets/template/.gitignore
vendored
Normal file
5
skills/app-builder/assets/template/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
7
skills/app-builder/assets/template/api/Dockerfile
Normal file
7
skills/app-builder/assets/template/api/Dockerfile
Normal file
@@ -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"]
|
||||
10
skills/app-builder/assets/template/api/drizzle.config.ts
Normal file
10
skills/app-builder/assets/template/api/drizzle.config.ts
Normal file
@@ -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!,
|
||||
},
|
||||
});
|
||||
23
skills/app-builder/assets/template/api/package.json
Normal file
23
skills/app-builder/assets/template/api/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
6
skills/app-builder/assets/template/api/src/db/index.ts
Normal file
6
skills/app-builder/assets/template/api/src/db/index.ts
Normal file
@@ -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 });
|
||||
54
skills/app-builder/assets/template/api/src/db/schema.ts
Normal file
54
skills/app-builder/assets/template/api/src/db/schema.ts
Normal file
@@ -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(),
|
||||
// });
|
||||
14
skills/app-builder/assets/template/api/src/index.ts
Normal file
14
skills/app-builder/assets/template/api/src/index.ts
Normal file
@@ -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}`);
|
||||
17
skills/app-builder/assets/template/api/src/lib/auth.ts
Normal file
17
skills/app-builder/assets/template/api/src/lib/auth.ts
Normal file
@@ -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,
|
||||
},
|
||||
});
|
||||
38
skills/app-builder/assets/template/api/src/routes/hammer.ts
Normal file
38
skills/app-builder/assets/template/api/src/routes/hammer.ts
Normal file
@@ -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
|
||||
@@ -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
|
||||
40
skills/app-builder/assets/template/docker-compose.yml
Normal file
40
skills/app-builder/assets/template/docker-compose.yml
Normal file
@@ -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:
|
||||
12
skills/app-builder/assets/template/web/Dockerfile
Normal file
12
skills/app-builder/assets/template/web/Dockerfile
Normal file
@@ -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;"]
|
||||
17
skills/app-builder/assets/template/web/nginx.conf
Normal file
17
skills/app-builder/assets/template/web/nginx.conf
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
28
skills/app-builder/assets/template/web/package.json
Normal file
28
skills/app-builder/assets/template/web/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
66
skills/app-builder/references/deploy.md
Normal file
66
skills/app-builder/references/deploy.md
Normal file
@@ -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.<app>.donovankelly.xyz` → api service port
|
||||
- Web: `app.<app>.donovankelly.xyz` → web service port
|
||||
4. Enable HTTPS (Dokploy handles Let's Encrypt)
|
||||
5. Deploy and verify health check
|
||||
|
||||
## Domain Pattern
|
||||
|
||||
- Test: `test-<app>.donovankelly.xyz`
|
||||
- Production: `<app>.donovankelly.xyz`
|
||||
- API: `api.<app>.donovankelly.xyz` (or `api.todo.donovankelly.xyz`)
|
||||
- Frontend: `app.<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
|
||||
47
skills/app-builder/references/legal-payments.md
Normal file
47
skills/app-builder/references/legal-payments.md
Normal file
@@ -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.
|
||||
49
skills/app-builder/references/migrations.md
Normal file
49
skills/app-builder/references/migrations.md
Normal file
@@ -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
|
||||
```
|
||||
43
skills/app-builder/references/rollback.md
Normal file
43
skills/app-builder/references/rollback.md
Normal file
@@ -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
|
||||
55
skills/app-builder/scripts/scaffold.sh
Executable file
55
skills/app-builder/scripts/scaffold.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# Scaffold a new app from the standard template
|
||||
# Usage: scaffold.sh <app-name> [--api-only]
|
||||
#
|
||||
# Creates project structure, initializes git, and pushes to Gitea.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APP_NAME="${1:?Usage: scaffold.sh <app-name>}"
|
||||
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"
|
||||
Reference in New Issue
Block a user