Thank you for contributing to stellar_Earn! This guide covers the standards every contributor must follow to keep the codebase consistent, secure, and maintainable.
- Getting Started
- Branch & Commit Conventions
- Pull Request Requirements
- NestJS Best Practices
- Testing Standards
- Swagger / API Documentation
- Smart Contract (Rust / Soroban)
- Reviewer Checklist
- Labels
- Script Inventory
| Tool | Min Version | Required? | Notes |
|---|---|---|---|
| Node.js | ≥ 22 | ✅ Required | Use nvm or fnm |
| npm | ≥ 10 | ✅ Required | Bundled with Node 22+ |
| Bun | ≥ 1 | Used for migration commands (bun run migration:*) |
|
| TypeScript (tsc) | ≥ 5 | ✅ Required | Installed via npm ci as devDependency |
| Git | ≥ 2 | ✅ Required | |
| Rust / Cargo | ≥ 1.80 (stable) | ✅ Required | Install via rustup |
| PostgreSQL | ≥ 14 | ✅ Required | Local dev; Docker image works fine |
| Redis | ≥ 7 | ✅ Required | Local dev; Docker image works fine |
| Docker | ≥ 24 | Easiest way to run Postgres + Redis locally | |
Stellar CLI (stellar) |
≥ 22 | Required to build/deploy Soroban contracts |
Automated check: Run the onboarding preflight script to validate all CLI tool versions in one go:
cd BackEnd && npm install && npm run check:toolchainRequired tools failing → exit code 1. Recommended tools failing → warning only.
# Backend
cd BackEnd
cp .env.example .env # Fill in your local values
npm install
npm run check:toolchain # Verify your local toolchain first
npm run typeorm:run-migrations
npm run start:dev
# Frontend
cd FrontEnd/my-app
npm install
npm run dev
# Contracts
cd Contract
cargo build --workspaceThis project uses npm as the package manager for the frontend (FrontEnd/my-app). The package-lock.json file must be committed to the repository and kept up to date.
- Always run
npm install(neveryarn,pnpm, orbun) insideFrontEnd/my-app. - Commit
package-lock.jsonwhenever you add, remove, or update dependencies. CI runsnpm ci, which requires a valid lockfile. - Do not delete or add
package-lock.jsonto.gitignore. - If you see a merge conflict in
package-lock.json, resolve it by runningnpm installafter resolvingpackage.json, then commit the regenerated lockfile.
<type>/<short-description>
| Type | When to use |
|---|---|
feat/ |
New feature |
fix/ |
Bug fix |
chore/ |
Tooling, CI, dependencies |
docs/ |
Documentation only |
refactor/ |
Code restructuring without behavior change |
test/ |
Tests only |
security/ |
Security-related fix |
Example: feat/quest-reward-distribution
Follow Conventional Commits:
<type>(scope): <short description>
[optional body]
[optional footer: Closes #<issue>]
Example:
feat(quests): add reward distribution via Soroban contract
Implements payout logic triggered on quest completion.
Uses BullMQ job queue to handle async Stellar transactions.
Closes #42
All PRs must use the PR template. PRs that skip required sections or are not linked to an issue will be blocked from merging.
- Branch is up to date with
main - Linked to an open GitHub issue
- PR template fully filled out (no placeholder text left)
- All CI checks pass (lint, format, build, tests)
- Self-reviewed every line of the diff
Keep PRs focused. If a PR touches more than ~400 lines (excluding migrations and generated files), consider splitting it. Large PRs take much longer to review and are more likely to introduce bugs.
Backend documentation belongs under BackEnd/docs/. Do not commit ad-hoc status/report markdown files to the root of BackEnd/ or the repository root — instead consolidate new content into an existing doc, create a topic-appropriate doc under BackEnd/docs/, or record the change in the relevant module CHANGELOG.md. One-off status reports that no longer reflect current state should not be committed at all.
Repository-maintained scripts are tracked in docs/script-inventory.md. When you add or change a script, update that inventory with:
- its purpose
- the owning team or function
- its lifecycle state
- any notable maintenance or verification notes
Every public endpoint must validate its input through a DTO. Raw, unvalidated request data must never reach the service layer.
- Create a dedicated DTO file:
src/modules/<module>/dto/<action>-<resource>.dto.ts - Use
class-validatordecorators for all fields - Use
class-transformerdecorators for type coercion and sanitization - Mark optional fields with
@IsOptional()before all other decorators - Use
@ApiProperty/@ApiPropertyOptionalon every field for Swagger
import { IsString, IsNotEmpty, IsOptional, IsUUID, MaxLength } from 'class-validator';
import { Transform } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateQuestDto {
@ApiProperty({ description: 'Quest title', maxLength: 120 })
@IsString()
@IsNotEmpty()
@MaxLength(120)
@Transform(({ value }) => value?.trim())
title: string;
@ApiPropertyOptional({ description: 'Optional description' })
@IsOptional()
@IsString()
@MaxLength(1000)
description?: string;
@ApiProperty({ description: 'Reward in stroops' })
@IsUUID()
rewardAssetId: string;
}// Never access req.body directly in a service or controller without a DTO
@Post()
create(@Body() body: any) { ... } // untyped bodyUse the appropriate NestJS HTTP exception class. Never throw a generic Error from a controller or service.
| Situation | Exception Class |
|---|---|
| Resource not found | NotFoundException |
| Malformed / invalid input | BadRequestException |
| Unauthenticated request | UnauthorizedException |
| Authenticated but not allowed | ForbiddenException |
| Duplicate / conflict state | ConflictException |
| Unprocessable business logic | UnprocessableEntityException |
| Upstream / third-party failure | ServiceUnavailableException |
import { NotFoundException, ForbiddenException } from '@nestjs/common';
async findQuest(id: string, userId: string): Promise<Quest> {
const quest = await this.questsRepository.findOne({ where: { id } });
if (!quest) {
throw new NotFoundException(`Quest ${id} not found`);
}
if (quest.ownerId !== userId) {
throw new ForbiddenException('You do not have access to this quest');
}
return quest;
}Wrap all contract and Horizon calls in try/catch and translate failures into NestJS exceptions:
try {
await this.sorobanService.invokeContract(payload);
} catch (err) {
this.logger.error('Soroban contract invocation failed', err.stack);
throw new ServiceUnavailableException('Blockchain transaction failed. Please retry.');
}throw new Error('Quest not found'); // generic Error
throw new HttpException('Forbidden', 403); // magic status code
if (!quest) return null; // silent failureThe project uses Winston via nest-winston. Inject LoggerService from @nestjs/common — do not use console.log anywhere in production code.
| Level | When to use |
|---|---|
error |
Exceptions, failed external calls, unexpected states |
warn |
Recoverable issues, deprecated usage, rate limit hits |
log (info) |
Significant lifecycle events (service started, job completed) |
debug |
Detailed flow tracing — only in development (NODE_ENV=development) |
verbose |
High-frequency events — disabled in production |
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class QuestsService {
private readonly logger = new Logger(QuestsService.name);
async completeQuest(id: string): Promise<void> {
this.logger.log(`Completing quest ${id}`);
try {
await this.processReward(id);
this.logger.log(`Quest ${id} completed and reward dispatched`);
} catch (err) {
this.logger.error(`Failed to complete quest ${id}`, err.stack);
throw err;
}
}
}- Passwords, JWT tokens, or refresh tokens
- Stellar secret keys (
S...) or contract IDs that are sensitive - Full request/response bodies containing user PII
- Raw database query results with credentials
The global LoggerMiddleware already handles request/response logging — do not duplicate it.
All endpoints requiring a signed-in user must be protected:
import { UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@CurrentUser() user: User) { ... }Use the @Public() decorator to explicitly opt out of authentication — do not remove or disable guards:
import { Public } from 'src/common/decorators/public.decorator';
@Public()
@Get('health')
healthCheck() { ... }Admin-restricted routes must use the admin guard in addition to JwtAuthGuard:
@UseGuards(JwtAuthGuard, AdminGuard)
@Delete('quests/:id')
adminDeleteQuest(@Param('id') id: string) { ... }The global AppThrottlerGuard applies to all routes. If a specific endpoint needs a custom limit use @Throttle():
import { Throttle } from '@nestjs/throttler';
@Throttle({ default: { limit: 5, ttl: 60000 } })
@Post('auth/login')
login(@Body() dto: LoginDto) { ... }// Never bypass guards with role checks inside the handler
@Get('admin/users')
getUsers(@CurrentUser() user: User) {
if (user.role !== 'admin') throw new ForbiddenException(); // use a guard instead
}- Co-locate spec files with the source:
<name>.service.spec.ts - Mock all external dependencies (repositories, external services, config)
- Aim for ≥ 80% branch coverage on all new services
- Run before pushing:
npm run test:cov
- Located in
BackEnd/test/ - Use
supertestagainst a test application instance - Each test must clean up its own database state
describe('QuestsService', () => {
describe('completeQuest', () => {
it('should dispatch a reward when quest exists and is pending', async () => { ... });
it('should throw NotFoundException when quest does not exist', async () => { ... });
it('should throw ForbiddenException when user does not own the quest', async () => { ... });
});
});- Run the app and verify changes at
http://localhost:3000/api/docs - Every controller method must have
@ApiOperation({ summary: '...' }) - Every possible response status must have an
@ApiResponsedecorator - DTOs must have
@ApiPropertyon every field — useexample:to show realistic values - Protected endpoints must have
@ApiBearerAuth()
-
All contract changes must pass
cargo test --workspaceandcargo clippy -- -D warnings -
Snapshot tests in
contracts/earn-quest/test_snapshots/must be updated when behavior changes -
Never expose raw contract errors to API consumers — translate them in the NestJS service layer
-
Soroban RPC calls must be async and non-blocking to the main request thread (use BullMQ jobs)
-
Changes to
contracts/earn-quest/src/**must update contracts/earn-quest/CHANGELOG.md -
Contract-breaking changes must use Conventional Commit breaking metadata (
type(scope)!:) and aBREAKING CHANGE:explanation -
Contract-breaking changelog entries must live under
## [Unreleased]->### Breaking Changesand include impact, affected files, and migration steps -
See contracts/earn-quest/docs/CHANGELOG_DISCIPLINE.md for the full policy and CI-enforced expectations
Use this checklist when reviewing any PR targeting the NestJS backend:
- Module boundaries respected — no cross-module direct repository access
- Business logic lives in the service layer, not controllers or guards
- New entities registered in
AppModuleTypeORM configuration - New modules imported in
AppModule(or the appropriate parent module)
- Every endpoint body/query has a typed DTO (no
anyor plain objects) -
class-validatordecorators are complete and sensible - Optional fields use
@IsOptional()before other decorators -
@ApiPropertypresent on all DTO fields
- Correct NestJS HTTP exception class used for every failure case
- No silent failures (returning
null/undefinedwithout throwing) - Stellar/Soroban failures are caught and translated to NestJS exceptions
- Global exception filter not bypassed
-
Loggerfrom@nestjs/commonused — noconsole.log - Errors logged with
err.stack - No sensitive data in log messages
- Log level is appropriate (not everything logged at
error)
- All authenticated routes use
JwtAuthGuard(or equivalent) - Admin routes have an additional admin guard
- Public routes explicitly marked with
@Public() - Throttler not disabled without justification
- Unit tests cover the happy path and all edge cases
- Tests use mocks — no real DB or network calls in unit tests
- E2E tests added for new endpoints
- All tests pass in CI
- No secrets or keys hardcoded in source
-
.env.exampleupdated for new env vars - Input sanitized via
class-transformer@Transformwhere needed - No new direct SQL queries bypassing TypeORM
- Migration created for schema changes
- Migration is reversible
- No
synchronize: truein non-development environments
| Label | Description |
|---|---|
backend |
Changes to the NestJS backend |
frontend |
Changes to the Next.js frontend |
contract |
Changes to Rust/Soroban smart contracts |
collaboration |
Process, templates, or contribution workflow |
developer-experience |
Tooling, CI/CD, or dev environment improvements |
priority-high |
Blocking issue or critical bug |
priority-medium |
Important but not blocking |
priority-low |
Nice to have |
security |
Security-related change — requires security review |
breaking-change |
Introduces a breaking API or contract change |
needs-review |
Ready for reviewer attention |
work-in-progress |
Not ready for review yet |
For questions, open a GitHub Discussion or ping a maintainer in the PR comments.
The frontend (FrontEnd/my-app/) maintains a Keep-a-Changelog-style
CHANGELOG.md so that every breaking
TypeScript type, interface, enum, Zod schema, or API model change is
announced and migratable.
Read the full policy:
FrontEnd/my-app/docs/TYPE_CHANGES_POLICY.md
In short:
-
If your PR modifies any file under
FrontEnd/my-app/lib/types/**,lib/api/**,lib/schemas/**,lib/validation/**, orcontext/walletTypes.ts, you must either updateCHANGELOG.mdor add a file inFrontEnd/my-app/.changeset/. -
For breaking changes the entry must include a before/after
Migration:code block. -
CI runs
npm run changelog:checkvia thefrontend-changelogworkflow. -
To bypass for a verified non-breaking change, add the
changelog-skiplabel or[changelog-skip]token to the PR title.
When developing backend features, always run these commands in this exact order:
npm run gql:build
## 9. Backend Development: Test & Code Quality Workflow
### 9.1 Available Commands
### 9.1 Available Commands
| Command | Purpose |
|---------|---------|
| `npm run test` | Run all tests (located in `tests/run-tests.js`) |
| `npm run lint-staged` | Run Prettier formatter on staged files |
| `npm run prepare` | Install git hooks (runs automatically after `npm install`) |
| `npm run typecheck` (if available) | Run TypeScript type checking (`tsc --noEmit`) |
**Note:** This project uses TypeScript (`.ts` files). If a `typecheck` script is not yet configured, run type checking manually with:
```bash
npx tsc --noEmit