Skip to content

Architecture

The web app, the API, the background worker and the code they share live in one Nx repository. Every request takes the same path, and every part has one job.

In short

  • React and Vite in front, NestJS and GraphQL behind, PostgreSQL and Redis underneath.
  • Slow work goes to a background worker, never into a request.
  • The web app's types are generated from the API's schema, so a renamed field fails the build.

A few packages, one repository

Nx runs each package's tasks and caches the results, and Yarn installs everything at once. Tags decide which package may import which, and a lint rule enforces them: the web app never imports server code.

  • Shared types and constants, such as plans and permissions, live in one package
  • Checks only run again on the packages a change touched
The packages of the repository
PackageWhat it holds
app-frontThe web app: React 19, Vite, Apollo Client, Jotai, Linaria, Lingui
app-serverThe API and the background worker: NestJS 11, GraphQL Yoga, TypeORM, PostgreSQL, Redis, BullMQ
app-sharedTypes, constants and helpers for both sides: routes, plans, permissions, languages
app-uiThe design system: tokens, components, icons, Storybook
app-websiteThis website, prerendered, with the docs
app-lint-rulesThe 17 custom lint rules
app-e2e-testingEnd-to-end tests: a browser clicks through the app
app-dockerDocker files: local services and the production stack
app-utilsScripts: setup, yarn verify, production and backups

The path of every request

A request goes through the same doors in the same order: sign-in, permission, input, then one service. Nothing reaches your data before all of them agree.

  • Resolvers stay thin: they read the signed-in context and call one service
  • Errors carry a code and a translated message, never internal details
  1. The web app sends the queryApollo adds the Clerk session token and the person's language.Authorization: Bearer <token>
  2. A middleware signs the request inIt verifies the token and swaps Clerk's ids for your own.RequestContextMiddleware
  3. Guards check who and whatFirst the authentication guard, then the permission the endpoint needs.WorkspacePermissionGuard('org:projects:create')
  4. The input is validatedOne global pipe checks every field and refuses unknown ones.
  5. A thin resolver calls one serviceThe service holds the rules of the feature.
  6. The repository adds the workspaceEvery query stays inside the signed-in member's workspace.WorkspaceScopedRepository

A worker for the slow work

The worker is a separate process that runs queued jobs from Redis with BullMQ. Webhooks, plan changes and scheduled jobs go through it, so requests stay fast and failed work is retried.

  • A job carries only ids and loads fresh data itself
  • Every job is safe to run twice, and alongside others
  • Scheduled jobs (crons) are written as classes and registered by the worker when it starts
  • When the worker stops, running jobs finish first

A stack chosen piece by piece

Every tool has a reason to be there, and the conventions explain each one, so you and your assistant know what to expect before changing it.

Each part of the stack, and why it is there
PartBuilt withWhy
FrontendReact 19, TypeScript, Vite, Linaria, Jotai, Apollo Client, GraphQLFast builds, typed data and styles that cost nothing at runtime.
BackendNestJS, GraphQL, PostgreSQL, TypeORM, Redis, BullMQA structured API, a relational database and background jobs.
ServicesClerk, Stripe, Docker, Caddy, SentrySign-in and payments run on specialists; production runs on containers, with automatic HTTPS and error alerts.
ToolingNx, oxlint, oxfmt, Jest, Vitest, PlaywrightOne repository, fast checks and tests at every level.

Questions

Why GraphQL?

One endpoint, one schema, and TypeScript types generated from it for the web app: a renamed field fails the build instead of a screen.

Why one database schema for every workspace?

A schema per workspace only pays off when customers create their own tables. One schema with a workspace column, guarded by the scoped repository, keeps migrations simple.

Where do I add my own feature?

Copy the Projects feature on both sides: it shows every rule applied. The guide "Add a feature" walks through it step by step.

Start your next app from a clean base

Pay once and get the full source code, with every future update.