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
| Package | What it holds |
|---|---|
app-front | The web app: React 19, Vite, Apollo Client, Jotai, Linaria, Lingui |
app-server | The API and the background worker: NestJS 11, GraphQL Yoga, TypeORM, PostgreSQL, Redis, BullMQ |
app-shared | Types, constants and helpers for both sides: routes, plans, permissions, languages |
app-ui | The design system: tokens, components, icons, Storybook |
app-website | This website, prerendered, with the docs |
app-lint-rules | The 17 custom lint rules |
app-e2e-testing | End-to-end tests: a browser clicks through the app |
app-docker | Docker files: local services and the production stack |
app-utils | Scripts: 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
- The web app sends the queryApollo adds the Clerk session token and the person's language.
Authorization: Bearer <token> - A middleware signs the request inIt verifies the token and swaps Clerk's ids for your own.
RequestContextMiddleware - Guards check who and whatFirst the authentication guard, then the permission the endpoint needs.
WorkspacePermissionGuard('org:projects:create') - The input is validatedOne global pipe checks every field and refuses unknown ones.
- A thin resolver calls one serviceThe service holds the rules of the feature.
- 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
ProcessStripeWebhookEventJobProcessClerkWebhookEventJobWebhookInboxRecoveryCronJobApplyWorkspaceMemberLimitJobProcessStripeWebhookEventJobA 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.
| Part | Built with | Why |
|---|---|---|
| Frontend | React 19, TypeScript, Vite, Linaria, Jotai, Apollo Client, GraphQL | Fast builds, typed data and styles that cost nothing at runtime. |
| Backend | NestJS, GraphQL, PostgreSQL, TypeORM, Redis, BullMQ | A structured API, a relational database and background jobs. |
| Services | Clerk, Stripe, Docker, Caddy, Sentry | Sign-in and payments run on specialists; production runs on containers, with automatic HTTPS and error alerts. |
| Tooling | Nx, oxlint, oxfmt, Jest, Vitest, Playwright | One 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.
Explore the other features
- Sign-in and workspacesClerk accounts, teams and roles checked in your code.
- BillingStripe subscriptions per workspace, driven by webhooks.
- AI guardrailsA rulebook, lint rules that explain, and yarn verify.
- Design system70 components, 466 icons and tokens for light and dark.
- SecurityLocked endpoints, rate limits, backups, alerts.
Start your next app from a clean base
Pay once and get the full source code, with every future update.