Scratchy Framework Architecture
Diátaxis type: Explanation — understanding-oriented, covers the why behind design decisions.
Table of Contents
- Overview
- High-Level Architecture
- Design Decisions
- Layer Responsibilities
- Request Lifecycle
- Error Handling Architecture
- Session and Cookie Management
- Security Layers
- Scalability Model
- Related Documentation
Overview
Scratchy is a full-stack TypeScript framework designed for building APIs and websites on hosted/dedicated servers. It is not a serverless framework — it is built for long-running Node.js processes with persistent connections, worker pools, and in-memory caching.
High-Level Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Client (Browser) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Qwik Pages │ │ React Islands│ │ Tailwind CSS Styles │ │
│ │ (Resumable) │ │ (qwikify$) │ │ (Utility-first) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────────────┘ │
│ │ │ │
│ ┌──────┴─────────────────┴──────┐ │
│ │ tRPC Client (internal) │ │
│ │ REST Client (external APIs) │ │
│ └──────────────┬────────────────┘ │
└─────────────────┼───────────────────────────────────────────────┘
│ HTTP/WebSocket
┌─────────────────┼───────────────────────────────────────────────┐
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Fastify Server │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────────────────────┐ │ │
│ │ │ tRPC │ │ REST │ │ Static Asset Serving │ │ │
│ │ │ /trpc/* │ │ /external/api/v1/* │ │ (Vite build output) │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────────────────────────┘ │ │
│ │ │ │ │ │
│ │ ┌────┴──────────────┴────────┐ │ │
│ │ │ Plugins & Middleware │ │ │
│ │ │ (Auth, CORS, Rate Limit, │ │ │
│ │ │ Helmet, Logging) │ │ │
│ │ └────────────┬───────────────┘ │ │
│ └───────────────┼───────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────────────────────────────────┐ │
│ │ ▼ │ │
│ │ ┌────────────────────┐ ┌──────────────────────────────┐ │ │
│ │ │ Piscina Worker │ │ Data Layer │ │ │
│ │ │ Pool │ │ │ │ │
│ │ │ ┌──────┐┌──────┐ │ │ ┌────────────┐ ┌────────┐ │ │ │
│ │ │ │ SSR ││ SSG │ │ │ │ Drizzle ORM│ │ Redis │ │ │ │
│ │ │ │Worker││Worker│ │ │ │ (PostgreSQL)│ │(Cache) │ │ │ │
│ │ │ └──────┘└──────┘ │ │ └────────────┘ └────────┘ │ │ │
│ │ └────────────────────┘ └──────────────────────────────┘ │ │
│ │ Node.js Server Process │ │
│ └────────────────────────────────────────────────────────────┘ │
│ Host Server │
└───────────────────────────────────────────────────────────────────┘Design Decisions
1. Server-First, Not Serverless
Decision: Build for hosted/dedicated servers with long-running processes.
Rationale:
- Persistent connections to databases and Redis reduce cold start overhead
- Worker Thread pools can be pre-warmed and kept alive
- In-memory caching (LRU, async-cache-dedupe) is effective with long-lived processes
- Connection pooling is straightforward without serverless lifecycle concerns
- WebSocket and SSE connections are natively supported
Trade-off: No auto-scaling per-request; scaling is done via horizontal server instances behind a load balancer.
2. Worker Threads for Rendering
Decision: Use Piscina Worker Thread pools for SSR and SSG instead of rendering on the main thread.
Rationale:
- SSR can be CPU-intensive (serializing component trees to HTML)
- Blocking the main thread would delay API responses
- Worker Threads have their own V8 isolate — garbage collection doesn't affect the main thread
- Piscina manages thread lifecycle, queuing, and resource limits
fastify-piscinaintegrates cleanly with Fastify's plugin system
Trade-off: Slightly higher memory usage (each worker has its own V8 heap) and communication overhead for small payloads.
3. Qwik as Primary Renderer (Not React)
Decision: Use Qwik for rendering with React as an escape hatch via qwikify$().
Rationale:
- Qwik's resumability means zero JavaScript is shipped until interaction
- Fine-grained lazy loading at the component and handler level
- Smaller initial JavaScript payload compared to React SSR + hydration
- React interop allows using the React ecosystem when needed
- Server-first architecture aligns with Qwik's design philosophy
Trade-off: Smaller ecosystem than React; developers need to learn Qwik's $() convention.
4. tRPC for Internal, REST for External
Decision: Use tRPC for all internal API communication and Fastify REST routes for external APIs.
Rationale:
- tRPC provides end-to-end type safety without code generation
- Internal clients (our own frontend) benefit from shared TypeScript types
- External consumers (third-party integrations) need standard REST with OpenAPI docs
- CORS is enabled only on
/external/apiroutes, reducing attack surface - tRPC's batching and streaming reduce round trips for internal use
Trade-off: Two API patterns to maintain, but the boundary is clear.
5. Drizzle ORM over Prisma
Decision: Use Drizzle ORM as the data layer.
Rationale:
- SQL-first approach — generated queries are predictable and inspectable
- No runtime query engine (unlike Prisma's Rust engine)
- Type-safe queries derived from schema definitions
- Lightweight — doesn't require a separate process or binary
- Schema definitions are plain TypeScript (easy to version control and review)
- Prepared statements are first-class citizens
- Supports raw SQL when needed without escaping the type system
Trade-off: Less "magic" than Prisma — requires writing more explicit queries.
6. Communication Patterns
Decision: Support both SharedArrayBuffer+Atomics and Redis for worker communication.
Rationale:
- SharedArrayBuffer for zero-copy, low-latency data sharing on a single server
- Redis (DragonflyDB) for distributed scenarios and cross-server state
- Let the developer choose based on deployment topology
- SharedArrayBuffer is ideal for large payloads (e.g., serialized component trees)
- Redis is ideal for cached data that multiple servers need to access
7. Convention-Based CLI Scaffolding
Decision: Provide CLI commands to scaffold models, views, APIs, and controllers.
Rationale:
- Reduces boilerplate and human error
- Enforces consistent project structure and naming conventions
- Inspired by Laravel's
artisanand RedwoodJS'sgeneratecommands - New team members can be productive immediately
- Generated code follows all framework conventions automatically
Layer Responsibilities
| Layer | Responsibility | Key Technology |
|---|---|---|
| Client | UI rendering, state management, user interaction | Qwik, React, Vite |
| API (Internal) | Type-safe RPC between client and server | tRPC, superjson |
| API (External) | RESTful endpoints for third-party consumers | Fastify routes, CORS |
| Server | HTTP handling, plugins, middleware, lifecycle management | Fastify |
| Workers | SSR, SSG, heavy computation off the main thread | Piscina, Worker Threads |
| Data | Database access, schema management, migrations | Drizzle ORM, PostgreSQL |
| Cache | Response caching, request deduplication | Redis/DragonflyDB, async-cache-dedupe |
| CLI | Project scaffolding, code generation | Custom CLI tool |
Request Lifecycle
Every incoming request passes through a layered pipeline inspired by Qwik City's onRequest and Remix's composable middleware:
Request → Fastify Hooks → Global Middleware → Route Middleware → Handler → Response| Stage | Runs | Docs |
|---|---|---|
| onRequest | Auth, rate limiting, early rejection | middleware.md |
| preValidation | CSRF token verification | security.md |
| preHandler | Authorization, session loading | sessions.md |
| handler | routeLoader$ / routeAction$ / route handler | data-loading.md |
| preSerialization | Response transformation | streaming.md |
| onSend | Security headers, compression | security.md |
| onResponse | Logging, metrics | middleware.md |
| onError | Error handler, error pages | error-handling.md |
Route-level middleware uses Qwik City's onRequest, onGet, onPost exports:
// src/client/routes/admin/layout.tsx
export const onRequest: RequestHandler = async (event) => {
if (!event.sharedMap.get("user")) {
throw event.redirect(302, "/login");
}
await event.next();
};See middleware.md for the full middleware architecture.
Error Handling Architecture
Scratchy provides layered error handling inspired by Next.js error boundaries, Remix ErrorBoundary exports, and Nuxt's createError():
| Layer | Pattern | Docs |
|---|---|---|
| Route errors | error.tsx per route segment | error-handling.md |
| Not found | not-found.tsx + notFound() helper | error-handling.md |
| Global errors | global-error.tsx root error page | error-handling.md |
| API errors | TRPCError codes + JSON envelope | api-design.md |
| Worker errors | Piscina error propagation + fallback | error-handling.md |
| Database errors | PostgreSQL error code mapping | error-handling.md |
See error-handling.md for full patterns.
Session and Cookie Management
Session handling uses patterns from Remix's cookie/session packages:
- Signed cookies with HMAC-SHA256 and secret rotation
- Multiple storage backends: Redis (production), PostgreSQL (audit), Cookie (small data)
- Flash messages for one-time notifications
- Session regeneration on authentication state changes
See sessions.md for full patterns.
Security Layers
- Helmet — Security headers (CSP, HSTS, X-Frame-Options) on all responses
- CORS — Enabled only on
/external/apiroutes - Rate Limiting — Per-route and global rate limits with Redis backend
- CSRF Protection — Double-submit cookie pattern on state-changing requests
- Authentication — Session-based via Better Auth (or equivalent)
- Authorization — tRPC middleware (isAuthenticated, isOwner, isAdmin)
- Input Validation — Zod schemas on all inputs (tRPC and REST)
- SQL Injection Prevention — Drizzle ORM parameterized queries
- CSP with Nonces — Content Security Policy compatible with Qwik resumability
See security.md for full security patterns.
Scalability Model
Load Balancer
┌─────────┐
│ Nginx / │
│ HAProxy │
└────┬────┘
┌──────────┼──────────┐
▼ ▼ ▼
┌─────────┐┌─────────┐┌─────────┐
│ Server 1 ││ Server 2 ││ Server 3 │
│ (Fastify)││ (Fastify)││ (Fastify)│
│ +Workers ││ +Workers ││ +Workers │
└────┬─────┘└────┬─────┘└────┬─────┘
│ │ │
└─────┬─────┘ │
▼ ▼
┌────────────┐ ┌────────────┐
│ PostgreSQL │ │ Redis │
│ (Primary + │ │ (DragonflyDB)
│ Replicas) │ │ │
└────────────┘ └────────────┘- Each server instance has its own Piscina worker pool
- PostgreSQL handles data persistence with connection pooling
- Redis handles caching, session storage, and inter-server communication
- Horizontal scaling by adding more server instances
Related Documentation
| Topic | Guide | Diátaxis Type |
|---|---|---|
| First-time setup | Getting Started | Tutorial |
| Directory layout | Project Structure | Reference |
| tRPC and REST | API Design | How-to / Reference |
| Drizzle ORM | Data Layer | How-to / Reference |
| SSR pipeline | Rendering | How-to / Explanation |
| HTML streaming | Streaming | How-to / Explanation |
| Piscina + SharedArrayBuffer | Worker Communication | How-to / Explanation |
| Sessions and cookies | Sessions | How-to / Reference |
| Defense-in-depth | Security | Reference |
| Hooks and guards | Middleware | How-to / Reference |
| Errors and boundaries | Error Handling | How-to / Reference |
| Nitro comparisons | Nitro Inspiration | Explanation |