Skip to content
Project structure

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.

PackageWhat it holdsTag
packages/app-frontThe web app: React 19, Vite, Apollo Client (GraphQL), Jotai (screen state), Linaria (styles), Lingui (translations)scope:frontend
packages/app-serverThe API server and the background worker: NestJS 11, GraphQL Yoga, TypeORM, PostgreSQL, Redis, BullMQ, Clerk, Stripescope:backend
packages/app-sharedTypes, 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-uiThe design system: tokens, components, icons, Storybookscope:shared
packages/app-websiteThe landing page (prerendered in English and French) and these docsscope:frontend
packages/app-lint-rulesThe 17 custom lint rules (an oxlint plugin)scope:shared
packages/app-e2e-testingThe end-to-end tests: Playwright clicks through the real appscope:e2e
packages/app-dockerDocker files: local Postgres and Redis, production images, stack and nightly backupsnone
packages/app-utilsScripts: local setup, yarn verify and the AI assistant hooks, forbidden-word check, production and backupsnone

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-shared and front-end packages, never the server;
  • the server may import app-shared, never the web app or app-ui;
  • app-shared and app-ui import only shared packages.

Inside the server

text
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 committed

The rule of direction: modules/ may use engine/, never the reverse.

Inside the web app

text
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 data

Two 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.md at the root: the short list of house rules, the local development commands and the known pitfalls ("Gotchas"). AGENTS.md is a link to the same file, for assistants that read that name.
  • docs/conventions/: the full rules, one file per area.
FileCovers
01-stack.mdThe libraries and versions, and why
02-monorepo-and-tooling.mdYarn, Nx, TypeScript, the formatter, the linter and its custom rules
03-code-style.mdTypes, exports, naming, comments, imports, shared helpers
04-frontend.mdComponents, state, GraphQL on the front end, styling, translations, forms, routing
05-backend.mdModules, guards, workspace isolation, database, errors, configuration, jobs
06-design-system.mdTokens, components, UX patterns, icons, accessibility, UI copy
07-testing.mdWhat to test where, and how
08-ci-and-git-workflow.mdPull requests, CI, drift checks, production changes
09-security.mdSecurity rules
10-auth-and-billing.mdClerk, 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

ThingRuleExample
Front-end component filePascalCase.tsx, one component per fileProjectRow.tsx
HookuseSomething.tsuseProjects.ts
Front-end utilcamelCase.ts, one function per filegetProjectStatusLabel.ts
Front-end constantPascalCase.ts, one SCREAMING_SNAKE_CASE exportProjectNameMaxLength.ts exports PROJECT_NAME_MAX_LENGTH
Server filekebab-case.<role>.tsproject.service.ts, project.resolver.ts, project.entity.ts
Server classnamed after its fileProjectService in project.service.ts
Props type<Component>PropsProjectRowProps
Styled componentStyled<Something>StyledHeader
Component that renders nothingends in EffectClerkSignOutEffect
Event handlershandleX for the function, onX for the prophandleDeleteConfirm, onClose
Acronymswritten as wordsApiKey, userId, never APIKey
Booleansread as questionsisOpen, 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

CheckCommandWhat it runs
Definition of doneyarn verifyEvery 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 CIyarn verify:fullEvery 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 formatnpx nx lint <package>oxlint with our custom rules (warnings count as errors), then the oxfmt formatter in check mode
Fix lint and formatnpx nx lint <package> --configuration=fixthe same, fixing what can be fixed automatically
Type checknpx nx typecheck <package>tsgo, the fast native preview of the TypeScript compiler
Unit testsnpx nx test <package>Jest in app-front, app-server, app-shared and app-website; Vitest in app-ui and app-lint-rules
Everythingnpx nx run-many -t lint typecheck testthe three checks in every package
Only what changednpx nx affected -t lint typecheck testthe 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 fileCommand
packages/app-server/generated/schema.graphqlnpx nx run app-server:graphql:schema
packages/app-front/src/generated/graphql.tsnpx 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 dictionarynpx 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.

Next