Project structure
Where each part of Neatship lives, which rules the code follows, how files are named, and how lint, type checks and tests run.
This guide is a map of the repository. After reading it, you will know which folder to open for a given change, where the written rules are, and which commands check your work.
One repository, several packages
Neatship is a monorepo: one git repository that holds several packages. Nx (the task runner) runs tasks such as lint or test for each package, and caches the results so unchanged packages are not checked twice. Yarn 4 installs the libraries for all packages at once.
| Package | What it holds | Tag |
|---|---|---|
packages/app-front | The web app: React 19, Vite, Apollo Client (GraphQL), Jotai (screen state), Linaria (styles), Lingui (translations) | scope:frontend |
packages/app-server | The API server and the background worker: NestJS 11, GraphQL Yoga, TypeORM, PostgreSQL, Redis, BullMQ, Clerk, Stripe | scope:backend |
packages/app-shared | Types, constants and helpers used by both sides: routes (APP_PATHS), plans (PLANS), permissions (WORKSPACE_PERMISSIONS), languages (APP_LOCALES), the product name (PRODUCT_NAME) | scope:shared |
packages/app-ui | The design system: tokens, components, icons, Storybook | scope:shared |
packages/app-website | The landing page (prerendered in English and French) and these docs | scope:frontend |
packages/app-lint-rules | The 17 custom lint rules (an oxlint plugin) | scope:shared |
packages/app-e2e-testing | The end-to-end tests: Playwright clicks through the real app | scope:e2e |
packages/app-docker | Docker files: local Postgres and Redis, production images, stack and nightly backups | none |
packages/app-utils | Scripts: local setup, yarn verify and the AI assistant hooks, forbidden-word check, production and backups | none |
The tag, set in each package's project.json, decides what a package may import. The app/enforce-module-boundaries lint rule enforces it:
- the web app and the landing page may import
app-ui,app-sharedand front-end packages, never the server; - the server may import
app-shared, never the web app orapp-ui; app-sharedandapp-uiimport only shared packages.
Inside the server
packages/app-server/
src/
main.ts the HTTP server entry point
queue-worker/ the background worker entry point (no HTTP)
app.module.ts the root module
engine/ the platform: reused by every feature
core-modules/ auth, billing, webhook, message-queue, app-config, logger,
secret-encryption, i18n, user, workspace, health...
api/graphql/ GraphQL errors, the error handler, schema generation
guards/ authentication and permission guards
decorators/ @AuthUser(), @AuthWorkspace(), validation helpers
middlewares/ request ids, and the middleware that signs each request in
workspace-scoped-repository/ data access limited to one workspace
modules/ your product features (Projects is the example)
database/
typeorm/core/migrations/ the database migrations
seeds/ the sample data
scripts/ seed and reset scripts
utils/custom-exception.ts
test/integration/ API tests against a real database
generated/schema.graphql the GraphQL schema, generated and committedThe rule of direction: modules/ may use engine/, never the reverse.
Inside the web app
packages/app-front/src/
modules/<feature>/ one folder per feature
components/ one component per file
effect-components/ components that render nothing and only run side effects
hooks/ useSomething.ts
graphql/ fragments/, queries/, mutations/ (one operation per file)
states/ screen state (Jotai atoms)
utils/ types/ constants/
__tests__/
pages/<area>/ thin route screens that assemble module components
generated/graphql.ts TypeScript types generated from the GraphQL schema
locales/ translation catalogs
testing/ test helpers and mock dataTwo import shortcuts replace relative paths: @/ points to src/modules/ and ~/ points to src/. Relative imports that go up a folder (../) are a lint error.
The rulebook
The written rules live in two places. Both are meant for people and for AI assistants.
CLAUDE.mdat the root: the short list of house rules, the local development commands and the known pitfalls ("Gotchas").AGENTS.mdis a link to the same file, for assistants that read that name.docs/conventions/: the full rules, one file per area.
| File | Covers |
|---|---|
01-stack.md | The libraries and versions, and why |
02-monorepo-and-tooling.md | Yarn, Nx, TypeScript, the formatter, the linter and its custom rules |
03-code-style.md | Types, exports, naming, comments, imports, shared helpers |
04-frontend.md | Components, state, GraphQL on the front end, styling, translations, forms, routing |
05-backend.md | Modules, guards, workspace isolation, database, errors, configuration, jobs |
06-design-system.md | Tokens, components, UX patterns, icons, accessibility, UI copy |
07-testing.md | What to test where, and how |
08-ci-and-git-workflow.md | Pull requests, CI, drift checks, production changes |
09-security.md | Security rules |
10-auth-and-billing.md | Clerk, Stripe and webhooks in detail |
Some sections of docs/conventions/ describe how to build pieces the starter doesn't include because none of its features needs them (for example a cache, feature flags, file uploads, emails, or a captcha on your own forms). They are marked Not included; none is needed to run in production, and each section says how to add the piece when your product needs it. Storybook covers the design system only: the web app has none (not included). When a rule and the code disagree, the code in the folder you are editing wins, as CLAUDE.md says: "the adjacent files in the directory you are editing beat any written rule".
Naming at a glance
| Thing | Rule | Example |
|---|---|---|
| Front-end component file | PascalCase.tsx, one component per file | ProjectRow.tsx |
| Hook | useSomething.ts | useProjects.ts |
| Front-end util | camelCase.ts, one function per file | getProjectStatusLabel.ts |
| Front-end constant | PascalCase.ts, one SCREAMING_SNAKE_CASE export | ProjectNameMaxLength.ts exports PROJECT_NAME_MAX_LENGTH |
| Server file | kebab-case.<role>.ts | project.service.ts, project.resolver.ts, project.entity.ts |
| Server class | named after its file | ProjectService in project.service.ts |
| Props type | <Component>Props | ProjectRowProps |
| Styled component | Styled<Something> | StyledHeader |
| Component that renders nothing | ends in Effect | ClerkSignOutEffect |
| Event handlers | handleX for the function, onX for the prop | handleDeleteConfirm, onClose |
| Acronyms | written as words | ApiKey, userId, never APIKey |
| Booleans | read as questions | isOpen, canDeleteProject |
A few more house rules: type instead of interface; string unions instead of enum (except GraphQL enums); never any; named exports only; no abbreviations; short // comments that explain why, never what. Before writing a helper, look in app-shared/utils (isDefined, assertUnreachable, parseJson, findOrThrow...): rewriting an existing helper is the most common mistake in AI-written code.
How the checks run
| Check | Command | What it runs |
|---|---|---|
| Definition of done | yarn verify | Every check below, on what changed compared to main, plus the generated files and the migrations; ends with "All checks passed" or the list of what to fix |
| Everything, like CI | yarn verify:full | Every check on every package, plus the builds, the API integration tests, the end-to-end tests and the design system's story tests |
| Lint and format | npx nx lint <package> | oxlint with our custom rules (warnings count as errors), then the oxfmt formatter in check mode |
| Fix lint and format | npx nx lint <package> --configuration=fix | the same, fixing what can be fixed automatically |
| Type check | npx nx typecheck <package> | tsgo, the fast native preview of the TypeScript compiler |
| Unit tests | npx nx test <package> | Jest in app-front, app-server, app-shared and app-website; Vitest in app-ui and app-lint-rules |
| Everything | npx nx run-many -t lint typecheck test | the three checks in every package |
| Only what changed | npx nx affected -t lint typecheck test | the checks in packages changed since the main branch |
Replace <package> with a package name: app-front, app-server, app-shared, app-ui, app-website or app-lint-rules.
Every TypeScript package uses strict mode and noUncheckedIndexedAccess (reading list[0] gives a value that may be undefined, so the code must handle it).
Tip: Nx can replay a cached type check that is out of date. To be sure, run the compiler directly inside the package: cd packages/app-front && npx tsgo -p tsconfig.json --noEmit.
Some files are generated and committed. yarn verify rewrites the ones your change affects and lists them; CI regenerates them and fails when the committed copy differs:
| Generated file | Command |
|---|---|
packages/app-server/generated/schema.graphql | npx nx run app-server:graphql:schema |
packages/app-front/src/generated/graphql.ts | npx nx run app-front:graphql:generate |
the translation catalogs in packages/app-front/src/locales/, packages/app-server/src/engine/core-modules/i18n/locales/ and packages/app-website/src/locales/ | npx nx run app-front:lingui:extract and npx nx run app-front:lingui:compile (same with app-server and app-website) |
| the design system's theme files and icon dictionary | npx nx run app-ui:generateTokens and npx nx run app-ui:generateIconDictionary |
The editor settings in .vscode/ format on save, fix lint problems on save and make automatic imports use the aliases. The custom lint rules don't run inside the editor: they run through nx lint, yarn verify, the Claude Code hook after each edit, and CI.