Contributing
CruzJS is open source and welcomes contributions. This guide covers how to set up your development environment, the contribution workflow, and the standards your code needs to meet.
Development Setup
Section titled “Development Setup”Prerequisites
Section titled “Prerequisites”- Node.js 20+
- pnpm —
npm install -g pnpm - Git
Clone and Install
Section titled “Clone and Install”git clone https://github.com/cruzjs/cruzjs.gitcd cruzjspnpm installFramework Monorepo Structure
Section titled “Framework Monorepo Structure”The CruzJS repository is a pnpm monorepo where the framework packages are developed and published:
cruzjs/├── packages/│ ├── core/ # @cruzjs/core — framework runtime│ ├── start/ # @cruzjs/start — UI components, theming│ ├── pro/ # @cruzjs/pro — billing, admin, audit logging│ ├── cli/ # @cruzjs/cli — unified CLI│ └── create-cruz-app/ # Project scaffolder├── apps/│ ├── web/ # Reference application (uses all packages)│ └── docs/ # Documentation site (Astro Starlight)├── external-processes/ # Standalone Workers, Workflows, Queue consumers└── tests/ # Unit and E2E testsThis is the framework development structure. End users who scaffold a project with npx create-cruz-app get a flat project that imports @cruzjs/* packages from npm — they do not have a packages/ directory.
Running the Reference App
Section titled “Running the Reference App”The apps/web directory contains a reference application that exercises all framework features. Use it for development and testing:
# Start the dev servercruz dev
# In another terminal, run database migrationscruz db migrate
# Optionally seed with test datacruz db seedRunning Tests
Section titled “Running Tests”# Unit testscruz test
# Unit tests in watch modecruz test --watch
# E2E testscruz test:e2e
# E2E tests with Playwright UIcruz test:e2e --ui
# Type checkingcruz typecheckBuilding Packages
Section titled “Building Packages”# Build all packagespnpm build
# Build a specific packagepnpm --filter @cruzjs/core buildContribution Workflow
Section titled “Contribution Workflow”1. Find or Create an Issue
Section titled “1. Find or Create an Issue”Before starting work, check the issue tracker for existing issues. If you’re proposing a new feature or significant change, open an issue first to discuss the approach.
2. Fork and Branch
Section titled “2. Fork and Branch”Fork the repository and create a branch from main:
git checkout -b feat/my-feature# orgit checkout -b fix/issue-123Branch naming conventions:
feat/description— New featuresfix/description— Bug fixesdocs/description— Documentation changesrefactor/description— Code refactoringtest/description— Test additions or fixes
3. Make Your Changes
Section titled “3. Make Your Changes”Follow the coding standards described below. Keep commits focused — one logical change per commit.
4. Write Tests
Section titled “4. Write Tests”All new features and bug fixes need tests:
- Services — Unit tests that mock the database and verify business logic
- Routers — Tests that verify procedures exist and call the correct service methods
- Components — Tests for non-trivial UI logic
- Bug fixes — A test that reproduces the bug and passes with the fix
5. Run the Full Check Suite
Section titled “5. Run the Full Check Suite”Before submitting, verify everything passes:
cruz typecheckcruz testcruz test:e2e6. Submit a Pull Request
Section titled “6. Submit a Pull Request”Push your branch and open a PR against main. The PR description should include:
- What — A clear summary of what changed
- Why — The motivation (link to the issue if applicable)
- How — A brief description of the implementation approach
- Testing — How you tested the change
Coding Standards
Section titled “Coding Standards”TypeScript Conventions
Section titled “TypeScript Conventions”Use type for object shapes, not interface:
// Preferredtype UserResponse = { id: string; email: string;};
// Only for extensible contractsinterface ModuleOptions { providers?: Provider[];}Use inline named exports:
// Correctexport const MyComponent: React.FC<Props> = ({ title }) => { ... };export type MyProps = { title: string };
// Incorrectconst MyComponent: React.FC<Props> = ({ title }) => { ... };export { MyComponent };Use braces for all control flow blocks:
// Correctif (!user) { throw new TRPCError({ code: 'NOT_FOUND' });}
// Incorrectif (!user) throw new TRPCError({ code: 'NOT_FOUND' });Use async/await, not raw promises:
// Correctconst user = await userService.getById(id);
// IncorrectuserService.getById(id).then(user => { ... });Naming Conventions
Section titled “Naming Conventions”| Type | Convention | Example |
|---|---|---|
| Package | @cruzjs/<name> | @cruzjs/core |
| Feature directory | kebab-case | user-profile |
| Service class | PascalCase + Service | NotesService |
| Router | camelCase + Router | notesRouter |
| Module class | PascalCase + Module | NotesModule |
| Provider | PascalCase + Provider | NotesProvider |
| Schema table | camelCase | notes, orgMembers |
| Validation schema | camelCase + Schema | createNoteSchema |
| Events | PascalCase + Event | NoteCreatedEvent |
| Types | PascalCase | NoteResponse |
Feature Module Structure
Section titled “Feature Module Structure”New features (in the reference app or user projects) should follow this file structure:
features/<name>/├── index.ts # Barrel exports├── <name>.module.ts # @Module decorator├── <name>.router.ts # tRPC router├── <name>.service.ts # Business logic├── <name>.schema.ts # Drizzle table definition├── <name>.validation.ts # Zod input schemas├── routes/ # Feature-specific React Router routes│ ├── index.tsx│ └── $id.tsx└── events/ # Domain events (if needed) ├── index.ts └── <event-name>.event.tsService Pattern
Section titled “Service Pattern”Services are @Injectable() classes that receive dependencies through constructor injection:
@Injectable()export class NotesService { constructor( @Inject(DRIZZLE) private readonly db: DrizzleDatabase, @Inject(EventEmitterService) private readonly events: EventEmitterService, ) {}
// Public methods first async list(orgId: string): Promise<Note[]> { ... } async create(orgId: string, userId: string, input: CreateNoteInput): Promise<Note> { ... }
// Private helpers last private toResponse(note: Note): NoteResponse { ... }}Router Pattern
Section titled “Router Pattern”Routers use orgProcedure for org-scoped data, protectedProcedure for user-scoped data, and publicProcedure for unauthenticated endpoints. Always verify org ownership before mutations:
export const notesRouter = router({ update: orgProcedure .input(z.object({ id: z.string(), data: updateNoteSchema })) .mutation(async ({ ctx, input }) => { await requirePermission(ctx.org, 'notes:write'); const container = await getAppContainer(); const service = container.resolve(NotesService);
// Always verify ownership const note = await service.getById(input.id); if (!note || note.orgId !== ctx.org.orgId) { throw new TRPCError({ code: 'NOT_FOUND' }); }
return service.update(input.id, input.data); }),});Database Rules
Section titled “Database Rules”- Use CUID for primary keys (
createId()) - Always add
orgIdfor organization-scoped data - Always add
createdByIdto track record ownership - Index all foreign keys
- Use cascade delete on foreign key references to organizations
- Export types with
$inferSelectand$inferInsert - Export all schemas from
src/database/schema.ts
Testing Standards
Section titled “Testing Standards”- Test behavior, not implementation details
- Use factory functions for consistent test data
- Mock external dependencies (database, external APIs)
- Test permission checks and ownership verification
- Use descriptive test names:
it('should return empty array when org has no notes')
describe('NotesService', () => { let service: NotesService; let mockDb: any;
beforeEach(() => { vi.clearAllMocks(); mockDb = { select: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), insert: vi.fn().mockReturnThis(), values: vi.fn().mockReturnThis(), returning: vi.fn(), }; service = new NotesService(mockDb); });
it('should filter notes by orgId', async () => { mockDb.orderBy.mockResolvedValue([]); await service.list('org-123'); expect(mockDb.where).toHaveBeenCalled(); });});Package Boundaries
Section titled “Package Boundaries”Understanding which packages to modify is important:
| Package | When to Modify |
|---|---|
packages/core/ | Adding framework-level capabilities (new DI features, new middleware, core service changes) |
packages/start/ | Adding shared UI components or theming |
packages/pro/ | Changes to organizations, billing, permissions, or admin features |
packages/cli/ | New CLI commands or changes to existing commands |
apps/web/ | Reference app changes, demonstrating new features |
apps/docs/ | Documentation |
Most contributions will touch packages/core/ or packages/pro/ for framework features, and apps/web/ plus apps/docs/ for the reference implementation and documentation.
Getting Help
Section titled “Getting Help”- Issues — Open an issue on GitHub for bugs or feature requests
- Discussions — Use GitHub Discussions for questions and ideas
- Code Review — All PRs are reviewed by maintainers. Expect feedback on architecture, naming, and test coverage