Project structure
Castor is a pnpm monorepo. Run every pnpm command from the repo root; the scripts in the root package.json forward to the relevant package.
Top level
castorjs/
├── package.json # Workspace root scripts (dev / verify / scaffold / db:* ...)
├── pnpm-workspace.yaml
├── apps/
│ ├── api/ # @castorjs/api: Fastify backend
│ ├── web/ # @castorjs/web: React frontend
│ └── mcp/ # @castorjs/mcp: MCP Server
├── docs/
│ ├── architecture.md # Architecture notes and design decisions
│ ├── frontend-design-system.md # Frontend UI system (shadcn/ui)
│ ├── apifox-full.openapi.json # OpenAPI document
│ ├── spec.schema.json # JSON Schema of module specs (scaffold --spec)
│ ├── examples/specs/ # Requirement → spec examples
│ ├── roadmap.md # Planned features (read the matching section before building one)
│ └── templates/ # Code scaffold templates (backend/, frontend/)
├── website/ # This docs site (VitePress; a standalone npm project, not in the pnpm workspace)
├── AGENTS.md # Shared project context for all AI tools
├── CLAUDE.md # Claude Code additions
├── .claude/ .agents/ .github/ # AI skills (.agents mirrors .claude/skills); .github holds CI, the docs deployment and issue templates
├── scripts/ # setup.sh (one-command Docker setup), docker-entrypoint.sh (image entry point)
└── Dockerfile / docker-compose.yml / render.yamlFor what each AI-related file does, see AI-driven workflow.
Backend: apps/api
apps/api/
├── src/
│ ├── main.ts # Web process entry point
│ ├── worker.ts # Entry point of the standalone scheduler process
│ ├── app.ts # buildApp(): plugins, routes, error handling, static assets, SPA fallback
│ ├── config.ts # Per-environment config (Zod-validated; refuses to start in production if key variables are missing)
│ ├── router.ts # Top-level route wiring; register new business domains here
│ ├── common/ # Cross-cutting concerns (main files only)
│ │ ├── auth.ts # loginRequired / hasMenuPermission / hasAnyMenuPermission / menuPermissionRequired
│ │ ├── rbac.ts # Pure permission-check functions
│ │ ├── csrf.ts # Double-submit CSRF check
│ │ ├── errors.ts # ServiceError and unified error handling
│ │ ├── db-errors.ts # Database constraint errors → 400 business errors
│ │ ├── http.ts # intParam / parseIntParam / notFound / queryString / getUploadedFile
│ │ ├── validation.ts # field.* / routeBody: request body declarations (Zod)
│ │ ├── pagination.ts # parsePagination (default 20, max 200)
│ │ ├── serialize.ts # toIso() and other time output helpers
│ │ ├── time-zone.ts # X-Time-Zone request header, withZoneOffset()
│ │ ├── data-scope.ts # Row-level data scope (resolveDataScope / dataScopeWhere)
│ │ ├── api-token.ts # Bearer API token authentication
│ │ ├── tabular.ts # csv / xlsx read and write
│ │ ├── i18n.ts # Translates response messages based on Accept-Language
│ │ ├── storage/ # File storage drivers (local / s3)
│ │ └── scheduler/ # Scheduled task runner, cron matcher, SSRF protection
│ ├── i18n/messages.ts # English / Japanese translations of backend errors
│ ├── demo/ # Public demo (DEMO_MODE) fixtures and reset
│ ├── db/
│ │ ├── client.ts # pg connection pool + Drizzle instance
│ │ ├── readonly.ts # Read-only connection pool for AI Data Query
│ │ ├── migrate.ts # Migration runner
│ │ ├── migrate-cli.ts # Entry point of pnpm db:migrate
│ │ └── schema/ # Model layer: Drizzle table definitions, one directory per domain, re-exported from index.ts;
│ │ # columns.ts holds the shared createdAt() / updatedAt() columns
│ └── modules/
│ ├── admin/ # System domain: auth / users / roles / departments / menu / logs / dicts /
│ │ # files / settings / sessions / two-factor / password-reset / api-tokens /
│ │ # webhooks / scheduled-task / notification / announcement / dashboard / assistant
│ └── component-center/ # Component Gallery domain: demo-record / ai-chat / ai-prompt / ai-sql /
│ # devtools / traffic-flow
├── drizzle/ # SQL migration files + meta/_journal.json (generated by drizzle-kit)
├── scripts/ # Toolchain: scaffold / verify-feature / seed-rbac / seed-demo / setup-once /
│ # init-ro-role / generate-openapi / import-apifox / demo-reset; lib/ holds shared helpers
├── test/ # Vitest, runs against a real PostgreSQL
└── drizzle.config.tsEach feature module consists of one table definition file and one module directory:
src/db/schema/<domain>/<name>.ts # Table definition + toDict serializer
src/modules/<domain>/<name>/schema.ts # Zod request schemas, import/export field maps
src/modules/<domain>/<name>/repository.ts # Database reads and writes
src/modules/<domain>/<name>/service.ts # Business logic
src/modules/<domain>/<name>/routes.ts # Routes and permission checksTable definitions live together under db/schema/ because drizzle-kit needs a single schema entry point. The other four layers sit next to each other per feature, so adding a feature only means creating files in one directory. For the layering rules, see Backend.
Frontend: apps/web
apps/web/
├── components.json # shadcn CLI config
├── scripts/
│ ├── shadcn-add.sh # Runs npx shadcn@latest add through a local relay
│ ├── i18n-scan.mjs # Scanner for untranslated text
│ └── api-types.mjs # Generates src/shared/api/openapi.d.ts from the OpenAPI document
├── test/ # Vitest (i18n, appearance, tabs bar, shared components, ...)
└── src/
├── App.tsx # Dynamic routing (resolves menu pages through lib/page-modules.ts)
├── index.css # Tailwind v4 entry + design tokens (light / dark / accent colors)
├── i18n/index.ts # i18next setup
├── locales/ # Shared UI translations; menus/ holds menu name translations
├── context/ # AuthContext / ThemeContext / TagsViewContext
├── components/
│ ├── ui/ # shadcn/ui primitives (source lives in the repo)
│ ├── ai-elements/ # AI chat building blocks (conversation, message, prompt input, ...)
│ └── app/ # App shell: AppLayout / AppSidebar / TopBar / TopNav / TagsView /
│ # AppearanceMenu / CommandMenu / LanguageSwitcher / ...
├── lib/ # page-modules (import.meta.glob page lookup) / cn / toast / format / motion /
│ # chart-theme / menu-icons / appearance / ...
├── modules/
│ ├── auth/pages/{login,reset_password}/ # Sign-in and password reset pages
│ ├── admin/{pages,api,components}/ # System pages, API clients and page components
│ └── component_center/
│ ├── pages/{patterns,components,dataviz,ai,editor,devtools}/
│ ├── showcase/ # Layout kit of the components/ pages (ShowcasePage / Example / PropsTable)
│ └── api/
└── shared/
├── api/request.ts # Axios instance (baseURL '/api'; sends the CSRF header, Accept-Language and X-Time-Zone automatically)
├── api/openapi.d.ts # API types generated from the OpenAPI document
├── hooks/ # useCrudList / useDebouncedValue / useIsMobile / useDictOptions / useAppInfo
├── utils/file.ts # downloadBlobFile
└── components/ # Shared business components: PageHeader / DataTable / FormDialog / ...A menu's component value <module>/<page_path> maps to the file modules/<module>/pages/<page_path>/index.tsx (for example admin/users → modules/admin/pages/users/index.tsx, component_center/patterns/kanban_page → modules/component_center/pages/patterns/kanban_page/index.tsx); a page anywhere else isn't found by dynamic routing. See Frontend.
MCP Server: apps/mcp
apps/mcp/src/index.ts wraps the toolchain as MCP tools for MCP clients such as Claude Desktop. See AI-driven workflow.
Naming conventions
| What | Rule | Example |
|---|---|---|
| Project and package names | Lowercase, hyphenated | castorjs, @castorjs/api |
| Backend directory and file names | Lowercase, hyphenated | component-center, scheduled-task |
| Database table names | Underscores | scheduled_tasks |
Frontend directories, menu component field | Underscores | component_center/patterns/card_list_page |
| API paths | Hyphenated, plural | /api/admin/customer-orders |
The session cookie name castor_session is an exception and uses an underscore.
