Skip to content

Architecture

The src/core/ directory contains the foundational infrastructure of saaskip. These modules are not features — they are the building blocks that features build on.

App-level configuration lives at src/config.ts — the first file to edit when customizing the boilerplate (title, description, URL, author, logos).

Structure

text
src/
  config.ts          → app metadata (title, description, URL, author, logos)
  core/
    async/           → withTimeout helper, TimeoutError
    auth/            → Better Auth schemas and role types
    env/             → typed environment variable validation
    db/              → Drizzle ORM client, health check
    errors/          → AppError class, error codes, message helpers
    helpers/         → shared utilities (string formatting)
    i18n/            → next-intl routing, messages, locale switcher
    mailer/          → Resend email client, template rendering, recipient whitelist
    middlewares/     → composable middleware chain + proxy entrypoint
    observability/   → Axiom logging, Sentry error tracking, request tracing, web vitals, health checks
    security/        → Arcjet, CSP, CSRF, body size limit, secure cookies, email whitelist
    seo/             → metadata, sitemap, robots, JSON-LD, PWA manifest helpers

async

FilePurpose
helpers/with-timeout.tsRuns a promise with a maximum timeout delay
errors/timeout-error.tsError thrown when an operation exceeds its timeout (504)

env

Validates environment variables at startup using @t3-oss/env-nextjs and zod. See Environment Variables for the full guide.

config (src/config.ts)

App-level metadata consumed by the root layout, emails, and metadata APIs:

ts
export const app = {
  title: 'saaskip',
  description: '...',
  url: env.NEXT_PUBLIC_APP_URL,
  author: 'oclio',
  logo: '/images/logo.svg',
};

This is the first file to edit when forking the boilerplate — change the title, description, author, and logos to match your product.

db

Type-safe database access via Drizzle ORM with a postgres-js connection pool. Includes query logging to Axiom and a health check for the /api/health endpoint. See Database for the full guide.

errors

FilePurpose
app-error.tsBase error class with code, statusCode, and context
codes.tsEnum of error codes (MIDDLEWARE_CHAIN_ERROR, UNKNOWN_ERROR, TIMEOUT)
helpers.tsgetErrorMessage() normalizes any thrown value to a string; formatErrorMessage() cleans and sentence-cases it

To create a domain-specific error, extend AppError:

ts
import { AppError, ErrorCode } from '@/core/errors';

class BillingError extends AppError {
  constructor(context?: Record<string, unknown>, cause?: unknown) {
    super(ErrorCode.UNKNOWN_ERROR, 'Billing failed', 400, context, { cause });
  }
}

helpers

Small, pure utilities shared across the codebase.

FunctionDescription
toSentence(text)Capitalizes first letter, adds trailing period if missing

i18n

Locale-prefixed routing, type-safe messages, and server/client translation access via next-intl. See Internationalization for the full guide.

mailer

Transactional email via Resend with React Email templates. Supports HTML, React elements, and named templates. Recipients are filtered through the email whitelist. See Mailer for the full guide.

middlewares

Next.js middleware is composed via a chain pattern instead of a single monolithic function.

FilePurpose
types/index.tsCustomMiddleware type — (req, event, next) => Promise<Response>
chain.tsComposes an array of middlewares into a single handler with next() dispatch
errors/middleware-chain-error.tsWraps non-AppError thrown inside the chain

The entrypoint is src/proxy.ts. It sets the x-pathname header on the request before calling the chain, so downstream middlewares and server components can access the original pathname. Middlewares are registered in src/proxy-stack.ts:

ts
import type { CustomMiddleware } from '@/core/middlewares/types';

const myMiddleware: CustomMiddleware = async (req, event, next) => {
  // do something before
  const response = await next();
  // do something after
  return response;
};

const stack: CustomMiddleware[] = [myMiddleware];
export default stack;

The chain runs middlewares in order, unwinds in reverse, and wraps any non-AppError into a MiddlewareChainError with the original message preserved in context.originalError.

Middleware stack

OrderMiddlewarePurpose
1withSecureCookiesEnforces HttpOnly, Secure, SameSite on response cookies
2withIntlLocale resolution — sets x-locale on response (all routes)
3withAxiomRequest logging and tracing via Axiom
4withCspContent-Security-Policy header
5withCsrfCSRF protection for state-changing requests
6withBodySizeLimitRejects requests exceeding the configured body size
7withArcjetRate limiting and bot detection via Arcjet

withSecureCookies is intentionally first (outermost) so it sees the final response after all other middlewares have set their Set-Cookie headers. See Security for details.

Header flow

HeaderSet byRead byPurpose
x-pathnameproxy.tscreatePageMetadataFull pathname (with locale prefix)
x-localewithIntlcreatePageMetadataResolved locale (en, fr, etc.)

observability

Structured logging, request tracing, web vitals via Axiom, error tracking via Sentry, and a /api/health endpoint for load balancers. See Observability for the full guide.

security

Defense-in-depth via composable middleware: CSP, CSRF, body size limit, secure cookies, email whitelist, and Arcjet for rate limiting and bot detection. Each layer can be independently enabled or disabled via environment variables. See Security for the full guide.

seo

SEO built entirely on the Next.js App Router metadata API — no external dependencies. Generates layout and page metadata from translated meta namespaces, a multilingual sitemap with hreflang alternates, robots.txt, OpenGraph and Twitter cards, JSON-LD structured data (WebSite and Organization schemas), and a PWA manifest with Apple touch icons. See SEO for the full guide.