Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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 }`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Health contract is inconsistent with the starter implementation.

Line 23 mandates { status, version, timestamp }, but Line 110 omits version. Please align the starter response with the stated API contract.

Proposed fix
-app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }));
+app.get('/health', (c) =>
+  c.json({
+    status: 'ok',
+    version: c.env.WORKER_VERSION ?? 'unknown',
+    timestamp: new Date().toISOString(),
+  })
+);

Also applies to: 110-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@rules/cloudflare-workers-hono-angular-saas-cursorrules-prompt-file/.cursorrules`
at line 23, The health endpoint implementation for GET /health does not match
the documented contract `{ status, version, timestamp }`; update the GET /health
handler to return an object including a status string (e.g., "ok"), a version
field (sourced from the app/package version or an env var), and a timestamp (ISO
string or epoch) so the runtime response matches the contract; ensure the
handler that constructs the response (the GET /health route) is changed to
include the version property.


## 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
- Workers Builds for native CI/CD (preview URLs per branch)

## 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)
- `step.realtime.publish()` for durable pub/sub
- 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
- `PROD_URL` env var for production testing

## 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
Comment on lines +66 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Hono (v4.12+), does secureHeaders() default configuration provide a strict nonce-based Content-Security-Policy suitable for Trusted Types enforcement, or must CSP nonce directives be configured explicitly?

💡 Result:

In Hono (v4.12+), the default configuration of secureHeaders does not provide any Content-Security-Policy (CSP) header, let alone a strict nonce-based one suitable for Trusted Types enforcement. CSP must be configured explicitly via the contentSecurityPolicy option. While the middleware supports nonce-based CSP (using the NONCE helper in directives like scriptSrc) and Trusted Types directives (trustedTypes and requireTrustedTypesFor), these require explicit configuration. The default SecureHeadersOptions sets other headers like Strict-Transport-Security, X-Frame-Options, etc., but CSP is "No Setting" by default.

Citations:


🏁 Script executed:

cat -n "rules/cloudflare-workers-hono-angular-saas-cursorrules-prompt-file/.cursorrules" | sed -n '60,120p'

Repository: PatrickJS/awesome-cursorrules

Length of output: 2104


🏁 Script executed:

# Search for CSP or Content-Security-Policy configuration in the codebase
rg -i "content-security-policy|csp|nonce" --type-list

Repository: PatrickJS/awesome-cursorrules

Length of output: 462


🏁 Script executed:

# Look for secureHeaders usage and any CSP configuration
rg -i "secureHeaders|contentSecurityPolicy" -A 5 -B 2

Repository: PatrickJS/awesome-cursorrules

Length of output: 55


The starter code must include explicit CSP configuration to match the documented security requirements.

The rules require nonce-based strict CSP as mandatory (line 66), but the starter code (lines 108–110) uses only secureHeaders(), which provides no Content-Security-Policy by default. Per Hono's documentation, CSP headers must be configured explicitly via the contentSecurityPolicy option. Add an explicit CSP configuration with nonce-based directives and Trusted Types enforcement to the middleware setup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@rules/cloudflare-workers-hono-angular-saas-cursorrules-prompt-file/.cursorrules`
around lines 66 - 69, The secureHeaders() middleware is being used without an
explicit Content-Security-Policy; update the middleware setup that calls
secureHeaders() to pass the contentSecurityPolicy option and configure a
nonce-based strict CSP with Trusted Types enforcement and required directives
(e.g., default-src 'none'; script-src 'nonce-<generated-nonce>'
'strict-dynamic'; style-src 'nonce-<generated-nonce>'; connect-src, img-src,
font-src as needed), ensuring the nonce is generated per request and injected
into responses and into any inline scripts/styles; modify the code that
generates responses to expose the per-request nonce to templates or inline
script insertion so the CSP nonce and Trusted Types policy are consistently
applied.

- Stripe webhooks: verify signature, deduplicate via KV

## Auth (Clerk)
- JWT verified per-request (no session store)
- Webhook sync: Clerk → D1 for user data
- RBAC: Clerk org roles for org-scoped, D1 for app-level
- Route layers: public → auth-only → role-gated → owner-only

## 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
- Functions ≤50 lines, cyclomatic complexity ≤10

## Deploy
```bash
npx wrangler deploy && curl -sX POST \
"https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"purge_everything":true}'
```

## Hono Worker 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;
```