Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions rules/cloudflare-workers-hono-angular-saas.md
Original file line number Diff line number Diff line change
@@ -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<AppType>(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;
```