Skip to content

Scratchy Framework Architecture

Diátaxis type: Explanation — understanding-oriented, covers the why behind design decisions.

Table of Contents


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-piscina integrates 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/api routes, 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 artisan and RedwoodJS's generate commands
  • New team members can be productive immediately
  • Generated code follows all framework conventions automatically

Layer Responsibilities

LayerResponsibilityKey Technology
ClientUI rendering, state management, user interactionQwik, React, Vite
API (Internal)Type-safe RPC between client and servertRPC, superjson
API (External)RESTful endpoints for third-party consumersFastify routes, CORS
ServerHTTP handling, plugins, middleware, lifecycle managementFastify
WorkersSSR, SSG, heavy computation off the main threadPiscina, Worker Threads
DataDatabase access, schema management, migrationsDrizzle ORM, PostgreSQL
CacheResponse caching, request deduplicationRedis/DragonflyDB, async-cache-dedupe
CLIProject scaffolding, code generationCustom 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
StageRunsDocs
onRequestAuth, rate limiting, early rejectionmiddleware.md
preValidationCSRF token verificationsecurity.md
preHandlerAuthorization, session loadingsessions.md
handlerrouteLoader$ / routeAction$ / route handlerdata-loading.md
preSerializationResponse transformationstreaming.md
onSendSecurity headers, compressionsecurity.md
onResponseLogging, metricsmiddleware.md
onErrorError handler, error pageserror-handling.md

Route-level middleware uses Qwik City's onRequest, onGet, onPost exports:

typescript
// 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():

LayerPatternDocs
Route errorserror.tsx per route segmenterror-handling.md
Not foundnot-found.tsx + notFound() helpererror-handling.md
Global errorsglobal-error.tsx root error pageerror-handling.md
API errorsTRPCError codes + JSON envelopeapi-design.md
Worker errorsPiscina error propagation + fallbackerror-handling.md
Database errorsPostgreSQL error code mappingerror-handling.md

See error-handling.md for full patterns.

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

  1. Helmet — Security headers (CSP, HSTS, X-Frame-Options) on all responses
  2. CORS — Enabled only on /external/api routes
  3. Rate Limiting — Per-route and global rate limits with Redis backend
  4. CSRF Protection — Double-submit cookie pattern on state-changing requests
  5. Authentication — Session-based via Better Auth (or equivalent)
  6. Authorization — tRPC middleware (isAuthenticated, isOwner, isAdmin)
  7. Input Validation — Zod schemas on all inputs (tRPC and REST)
  8. SQL Injection Prevention — Drizzle ORM parameterized queries
  9. 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
TopicGuideDiátaxis Type
First-time setupGetting StartedTutorial
Directory layoutProject StructureReference
tRPC and RESTAPI DesignHow-to / Reference
Drizzle ORMData LayerHow-to / Reference
SSR pipelineRenderingHow-to / Explanation
HTML streamingStreamingHow-to / Explanation
Piscina + SharedArrayBufferWorker CommunicationHow-to / Explanation
Sessions and cookiesSessionsHow-to / Reference
Defense-in-depthSecurityReference
Hooks and guardsMiddlewareHow-to / Reference
Errors and boundariesError HandlingHow-to / Reference
Nitro comparisonsNitro InspirationExplanation