diff --git a/rules/cloudflare-workers-hono-angular-saas.md b/rules/cloudflare-workers-hono-angular-saas.md new file mode 100644 index 0000000..d418869 --- /dev/null +++ b/rules/cloudflare-workers-hono-angular-saas.md @@ -0,0 +1,113 @@ +--- +title: Cloudflare Workers + Hono + Angular SaaS +description: Full-stack SaaS patterns for Cloudflare Workers with Hono API, Angular frontend, and enterprise integrations +tags: + - cloudflare + - workers + - hono + - angular + - saas + - typescript + - d1 + - drizzle +author: Brian Zalewski +source: https://github.com/megabytespace/claude-skills +--- + +# Cloudflare Workers + Hono + Angular SaaS + +Full-stack SaaS on Cloudflare Workers with Hono API, Angular frontend, and enterprise integrations. + +## Stack +CF Workers+Hono v4.12+ | Angular 21+Ionic 8+PrimeNG 21 | D1/Neon | Drizzle v1 | Zod | Clerk Core 3 | Stripe | Inngest v4 | Resend | Bun 1.3 | Playwright v1.59+ | Vitest | ESLint+Prettier | PostHog | Sentry + +## TypeScript +- Strict mode, never `any` (use `unknown`), prefer `interface` over `type` +- `readonly` when not reassigned, `undefined` over `null` +- Zod as source of truth for validation +- ESLint flat config (`eslint.config.ts`) + typescript-eslint + Prettier + +## Hono API +- Inline handlers for RPC type inference (never separate controller files) +- Method chaining: `app.use().get().post()` preserves types +- `hc(BASE_URL)` for typed client +- `@hono/zod-validator` on ALL request bodies +- `app.onError()` + `app.notFound()` centralized +- Split large apps: `app.route('/path', subApp)` +- Error envelope: `{ error: string, code?: string, details?: unknown }` +- `createFactory<{ Bindings: Env }>()` for reusable middleware chains +- `GET /health` returns `{ status, version, timestamp }` + +## Angular +- Standalone only (no NgModules), Angular 21 zoneless by default +- Signals stable: `signal()`, `computed()`, `effect()`, `linkedSignal()`, `resource()` +- `HttpResource` for data fetching +- Control flow: `@if`/`@for`/`@switch`/`@defer` (not `*ngIf`/`*ngFor`) +- kebab-case files, one component per file, `providedIn: 'root'` +- PrimeNG for UI components + +## Drizzle v1 +- `sqliteTable` for D1, plural snake_case tables +- `$inferSelect`/`$inferInsert` for types +- `createInsertSchema`/`createSelectSchema` from `drizzle-orm/zod` +- Batch API (not `BEGIN` — D1 doesn't support transactions) +- Prepared statements for repeated queries + +## CF Workers +- CPU limit: 10ms free / 30s paid +- `ctx.waitUntil()` for async post-response work +- `ctx.passThroughOnException()` for graceful degradation +- Bindings typed via `Env` interface +- D1 global read replication for latency reduction + +## Inngest v4 (Background Jobs) +- `eventType('name', { schema: z.object({...}) })` per-event (v4 breaking) +- `inngest/cloudflare` adapter + `inngest.setEnvVars(c.env)` for Workers +- Step functions: `step.run()`, `step.sleep()`, `step.waitForEvent()`, `step.sendEvent()` +- `step.ai.infer()` offloads inference (zero compute during wait) +- Each step idempotent, retried independently + +## Testing (TDD) +- Failing test FIRST, then implement +- Playwright for E2E: 6 breakpoints (375, 390, 768, 1024, 1280, 1920) +- Vitest for unit tests +- No sleeps — use `waitFor`/`toBeVisible()` +- Selectors: `data-testid` > role > text +- axe-core 0 violations + +## Security (OWASP Top 10:2025) +- Must: HSTS, CSP (nonce-based strict), X-Content-Type-Options, X-Frame-Options +- Must: Referrer-Policy, Permissions-Policy, COOP, COEP, CORP +- Remove: X-XSS-Protection, Expect-CT, Server, X-Powered-By +- Turnstile on all forms, Zod validation on all inputs + +## Auth (Clerk) +- JWT verified per-request (no session store) +- Webhook sync: Clerk → D1 for user data +- RBAC: Clerk org roles + D1 app-level roles + +## Quality +- Lighthouse: a11y ≥95, perf ≥75 +- WCAG 2.2 AA compliance +- LCP ≤2.5s, CLS ≤0.1, INP ≤200ms +- JS ≤200KB gz, CSS ≤50KB gz + +## Starter +```typescript +import { Hono } from 'hono'; +import { secureHeaders } from 'hono/secure-headers'; +import { cors } from 'hono/cors'; + +interface Env { + DB: D1Database; + KV: KVNamespace; + AI: Ai; + TURNSTILE_SECRET: string; +} + +const app = new Hono<{ Bindings: Env }>(); +app.use('*', secureHeaders()); +app.use('/api/*', cors({ origin: ['https://yourdomain.com'] })); +app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() })); +export default app; +```