Thank you for contributing to the TrustLink backend! This service is the automation layer that bridges the physical world and the blockchain — watching for on-chain events, tracking shipments, triggering fund releases, and keeping buyers and vendors informed in real time.
Your contributions keep the oracle running cleanly and securely.
- Code of Conduct
- Stellar Wave Program
- Before You Start
- Development Setup
- Project Structure
- Making Changes
- Coding Standards
- Commit Convention
- Pull Request Process
- Testing
- Database Migrations
- Security Vulnerabilities
- Getting Help
This is a welcoming project. Be constructive, be patient with newcomers, and assume good intent. We're building financial infrastructure for social commerce in underserved markets — the stakes are real and the community reflects that seriousness.
Harassment, dismissiveness, or toxic behaviour will not be tolerated.
This repository participates in the Stellar Wave Program — a sprint-based contribution program funded by the Stellar Development Foundation where developers earn real XLM rewards for resolving open issues.
- Browse
Stellar Waveandgood first issueissues - Sign in at drips.network/wave with GitHub
- Apply to the issue with a brief note on your approach
- Get assigned → build → open a PR before the Wave cycle closes
- Merged PR = Points = XLM rewards
| Label | Points | Scope |
|---|---|---|
complexity: trivial |
100 pts | Swagger docs, missing validation, small bug |
complexity: medium |
150 pts | New endpoint, new service method, integration test |
complexity: high |
200 pts | New module, background worker, external API integration |
⚡ Apply before the Wave sprint is fully subscribed. Maintainers move quickly during active cycles.
Before contributing, read SPECIFICATION.md — it is the authoritative specification for this project. Every code change must conform to its requirements. PRs that violate the specification will be rejected.
Key requirements from the SRD:
- No
anytypes — strict TypeScript required - Tests required for all new code — coverage of
src/must stay at or above 95% lines / 82% branches / 90% functions, enforced byscripts/check_coverage.js(issue #850 raised these floors from 70% / 64% / 65%) - Security first — JWT signature verification, input sanitization, PII encryption
- CI must pass — lint, test, integration tests and migrations are required checks. Dependency auditing runs weekly in
security-audit.ymland does not block merges - Conventional commits —
type(scope): description - Repository pattern — services never call Prisma directly
- New to NestJS or Node.js? → Start with
good first issue. These have full context and don't require deep Stellar knowledge. - Backend-experienced? → Look for
complexity: mediumorcomplexity: highissues. - Have an idea? → Open a GitHub Discussion before writing code. An upfront conversation prevents your PR from being closed due to design misalignment.
- Scan the issue thread for maintainer comments — the scope may have changed since it was filed.
- Check for an existing open PR on the issue — don't duplicate effort.
- For anything that touches the Stellar transaction signing flow, the auto-release worker, or the dispute resolution logic — leave a comment describing your proposed approach and wait for a maintainer thumbs-up before starting.
| Tool | Version | Notes |
|---|---|---|
| Node.js | 22 |
Pinned in .nvmrc. Run nvm use in the project root |
| npm | 10.x |
Ships with Node 22. See the note below before using another version |
| PostgreSQL | 15+ |
Local install or Docker |
| Docker | optional | For spinning up Postgres quickly |
Use Node 22, not just "20 or newer". Every CI workflow reads
.nvmrc, so anything else means your local results and CI's can disagree. Newer versions of npm in particular resolve optional platform dependencies differently and will produce apackage-lock.jsonthat makesnpm cifail in CI withMissing: ... from lock file. If you see that error, you are on the wrong Node version.Keep Node type definitions aligned. The
@types/nodemajor inpackage.jsonmust always match the runtime major in.nvmrc. Update both together, runnvm use, and regeneratepackage-lock.jsonwithnpm install. Do not merge Dependabot updates that move@types/nodepast the runtime major.
# 1. Fork and clone
git clone https://github.com/YOUR_USERNAME/trust-link-backend
cd trust-link-backend
# 2. Add upstream
git remote add upstream https://github.com/JSE-ORG/trust-link-backend
# 3. Use the pinned Node version
nvm use # reads .nvmrc
# 4. Install dependencies
npm ci # not `npm install` — see note below
# 5. Set up environment
cp .env.example .env
# Edit .env — testnet values are pre-filled, you need your own DB URL
# 6. Generate the Prisma client
npx prisma generate
# 7. Start Postgres (Docker shortcut)
docker run --name trustlink-pg \
-e POSTGRES_USER=trustlink \
-e POSTGRES_PASSWORD=trustlink \
-e POSTGRES_DB=trustlink_dev \
-p 5432:5432 -d postgres:15
# 8. Run migrations
npx prisma migrate dev
# 9. Seed the database (optional — creates test escrow records)
npm run db:seed
# 10. Start the dev server
npm run start:devThe API will be available at http://localhost:3000.
Swagger docs: http://localhost:3000/api/docs
Step 4, why npm ci. It installs exactly what the lockfile specifies and
never rewrites it. npm install will silently update package-lock.json, and
committing that drift is the single most common reason a pull request fails
every check at the install step. Only run npm install when you are
deliberately adding or upgrading a dependency.
Step 6, why prisma generate. The Prisma client is generated into
node_modules and is not committed. Skip this and @prisma/client exports do
not exist, so npm run typecheck fails with errors like
Module '"@prisma/client"' has no exported member 'PrismaClient'. That is a
missing step, not a broken repository. Re-run it whenever
prisma/schema.prisma changes. Note that prisma generate reads
prisma.config.ts, which calls env('DATABASE_URL') at load time — that is
why the .env file must exist before this step.
JwtGuard requires a properly signed SEP-10 access token. Sending a bare
Stellar address as a bearer token does not authenticate, so
.set('Authorization', `Bearer ${address}`) will return 401.
Use the helper in test/auth-helper.ts:
import { bearer } from '../auth-helper';
// Ordinary caller
.set('Authorization', bearer(VENDOR_ADDRESS))
// Admin caller
.set('Authorization', bearer(ADMIN_ADDRESS, { role: 'admin' }))It signs tokens with the same algorithm and claim shape as
Sep10Service.issueJwt, so a test token is indistinguishable from a real one to
the guard. test/integration/dispute-access.integration-spec.ts is a worked
example covering both cases.
If you branched before this landed, rebase onto dev. Tests that authenticated
with a bare address were migrated across the whole suite, and yours will need
the same change.
git fetch upstream
git rebase upstream/maintrustlink-backend/
├── src/
│ ├── app.module.ts # Root NestJS module — registers all modules
│ │
│ ├── escrow/ # Escrow module
│ │ ├── escrow.module.ts
│ │ ├── escrow.controller.ts # HTTP routes
│ │ ├── escrow.service.ts # Business logic
│ │ ├── escrow.repository.ts # DB queries (Prisma)
│ │ └── dto/
│ │ ├── create-escrow.dto.ts
│ │ └── update-shipment.dto.ts
│ │
│ ├── dispute/ # Dispute module
│ │ ├── dispute.module.ts
│ │ ├── dispute.controller.ts
│ │ ├── dispute.service.ts
│ │ └── dto/
│ │
│ ├── auth/ # SEP-10 authentication
│ │ ├── auth.module.ts
│ │ ├── auth.controller.ts # /auth/challenge + /auth/verify
│ │ ├── auth.service.ts
│ │ └── guards/
│ │ ├── jwt.guard.ts
│ │ └── admin.guard.ts
│ │
│ ├── stellar/ # Stellar SDK layer
│ │ ├── stellar.module.ts
│ │ ├── blockchain-listener.service.ts # SSE stream from Horizon
│ │ ├── contract.service.ts # Soroban contract interactions
│ │ └── horizon.service.ts # Horizon API utilities
│ │
│ ├── notifications/ # Email + SMS
│ │ ├── notifications.module.ts
│ │ ├── notifications.service.ts
│ │ └── templates/ # Email/SMS message templates
│ │
│ ├── logistics/ # Shipping carrier integrations
│ │ ├── logistics.module.ts
│ │ ├── logistics.service.ts # Carrier-agnostic interface
│ │ └── providers/
│ │ ├── terminal-africa.provider.ts
│ │ └── gigl.provider.ts
│ │
│ ├── workers/ # Background jobs
│ │ ├── auto-release.worker.ts # Cron: checks 48h delivery window
│ │ └── tracking-poll.worker.ts # Cron: polls carrier APIs
│ │
│ └── admin/ # Admin-only module
│ ├── admin.module.ts
│ ├── admin.controller.ts
│ └── admin.service.ts
│
├── prisma/
│ ├── schema.prisma
│ ├── seed.ts
│ └── migrations/
│
└── test/
├── unit/
├── integration/
└── e2e/
Key rules:
- Business logic lives in
*.service.ts— controllers handle HTTP only (parse input, call service, return response) - Database access belongs in
*.repository.ts— services should not callprismadirectly - Stellar SDK calls belong in
src/stellar/— nostellar-sdkimports elsewhere - All background jobs go in
src/workers/— nosetIntervalor ad-hoc cron calls in services
This repository uses two long-lived branches.
| Branch | What it is | Do you branch from it? |
|---|---|---|
dev |
Default branch. Every pull request targets this. | Yes |
main |
Released baseline, promoted from dev once green. |
No |
Branch from dev, and open your pull request against dev. A pull request
targeting main will be retargeted or closed.
git checkout dev
git pull upstream dev
git checkout -b feat/your-feature-nameIf you opened a pull request before this changed, you do not need to redo
anything. Rebase onto dev and a maintainer will retarget it:
git fetch upstream
git rebase upstream/dev| Type | Pattern | Example |
|---|---|---|
| Feature | feat/description |
feat/gigl-logistics-provider |
| Bug fix | fix/description |
fix/auto-release-duplicate-sign |
| Tests | test/description |
test/tracking-poll-worker-specs |
| Docs | docs/description |
docs/add-swagger-to-dispute-dto |
| Refactor | refactor/description |
refactor/extract-escrow-repository |
| Chore | chore/description |
chore/upgrade-prisma-5.x |
Add an entry to the [Unreleased] section of CHANGELOG.md when a change
affects service consumers or operators: new or changed endpoints, request or
response contracts, authentication or authorization behavior, configuration,
deployment behavior, security fixes, or observable bug fixes. Group entries
under the Keep a Changelog headings and describe the outcome for users rather
than copying the commit subject.
Do not add an entry for internal-only work such as tests, CI, refactors with no observable behavior change, formatting, or dependency maintenance unless it changes a consumer-facing behavior or fixes a security issue.
- TypeScript strict mode is enabled —
anytypes will be flagged in review - A pre-commit hook runs
eslint --fixon your staged.tsfiles —npm ciinstalls it, so you rarely need to think about formatting before committing - Run
npm run lintbefore committing — ESLint with NestJS rules must pass clean - Run
npm run formatto apply Prettier — don't submit unformatted code - Prefer
async/awaitover raw.then()chains - All thrown errors should be NestJS
HttpExceptionsubclasses — neverthrow new Error()in a controller or service
// ✅ Controller: thin — parse, delegate, return
@Post()
async createEscrow(
@Body() dto: CreateEscrowDto,
@CurrentUser() vendor: AuthUser,
): Promise<EscrowResponseDto> {
return this.escrowService.createEscrow(dto, vendor.address);
}
// ✅ Service: logic lives here
async createEscrow(dto: CreateEscrowDto, vendorAddress: string): Promise<Escrow> {
const existing = await this.escrowRepository.findByVendorAndItem(vendorAddress, dto.itemRef);
if (existing) throw new ConflictException("Duplicate escrow for this item reference");
// ... create and return
}
// ❌ Wrong — business logic leaking into controller
@Post()
async createEscrow(@Body() dto: CreateEscrowDto) {
const existing = await this.prisma.escrow.findFirst({ where: { ... } }); // DB in controller
if (existing) throw new ConflictException(...);
}Every request body must have a DTO decorated with class-validator and @ApiProperty (Swagger):
// dto/create-escrow.dto.ts
import { IsString, IsNumber, IsPositive, MinLength } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class CreateEscrowDto {
@ApiProperty({ example: "Vintage Nike Jacket – Size M", description: "Item name shown to buyer" })
@IsString()
@MinLength(3)
itemName: string;
@ApiProperty({ example: 50, description: "Amount in USDC" })
@IsNumber()
@IsPositive()
amount: number;
@ApiProperty({ example: 604800, description: "Shipping window in seconds (default: 7 days)" })
@IsNumber()
@IsPositive()
shippingWindow: number;
}- All Soroban contract interactions go through
ContractServiceinsrc/stellar/ - Transaction submission must use retry logic — Stellar can temporarily reject due to sequence number contention
- The system signer key (
SYSTEM_SIGNER_SECRET) must never be logged, even partially - Always verify the transaction was included in a ledger after submission — don't trust a successful
submitresponse alone - Parse Soroban contract events by
topic+dataXDR, not by position
- Never commit
.envfiles - Never hardcode keys, secrets, or contract IDs in source files — always use
process.envvia theConfigService - The
.env.examplefile must be kept up to date — if you add a new env var, add it there with a placeholder and a comment
This repo uses Conventional Commits.
<type>(<scope>): <short imperative description>
[optional body]
[optional footer: Closes #123]
Types:
| Type | Use for |
|---|---|
feat |
New endpoint, service, module, or worker |
fix |
Bug fix |
test |
New or updated tests |
docs |
Swagger annotations, README, comments |
refactor |
Code restructuring with no behaviour change |
perf |
Performance improvement |
chore |
Dependencies, CI config, tooling changes |
security |
Vulnerability fix or hardening |
Examples:
git commit -m "feat(logistics): add Terminal Africa webhook handler"
git commit -m "fix(auto-release): prevent duplicate transaction submission on retry"
git commit -m "test(dispute): add integration test for admin resolve endpoint"
git commit -m "docs(escrow): add Swagger @ApiProperty to CreateEscrowDto"
git commit -m "security(auth): add replay protection to SEP-10 challenge verification"# Format
npm run format
# Lint — must be clean
npm run lint
# Type check
npm run typecheck
# Tests
npm run test
# Build (catches compilation errors)
npm run build- What changed — Plain description of the change
- Why — Link to the issue
- Tests written — Unit and/or integration tests for new code
- Swagger updated — All new/changed endpoints have
@ApiOperation,@ApiResponse, and DTO@ApiPropertydecorators - No secrets — No API keys, contract IDs, or env values hardcoded
- Migration included — If schema changed, the Prisma migration file is committed
-
Closes #123
## Summary
<!-- What does this PR do? -->
## Motivation
<!-- Why is this change needed? Link to the issue. -->
## Changes
<!-- Bullet-point key changes -->
-
-
## Testing
<!-- What tests were added? How was this manually tested? -->
## Migration
<!-- If database schema changed, describe it here -->
## Notes for Reviewer
<!-- Anything reviewers should focus on? -->
Closes #- Active Wave cycle: 48 hours
- Outside Wave: 5 business days
- 1 approving review required
- Changes to Stellar transaction signing, auto-release logic, or dispute resolution require 2 approvals
The repository maintains three test suites with strict filename regex boundaries, commands, and CI workflows:
| Suite | Filename Pattern | Command | CI Workflow | Selection Rule |
|---|---|---|---|---|
| Unit | (src|test)/.*\.spec\.ts$ |
npm test / npm run test:cov |
test.yml / ci.yml |
Use for isolated unit testing of a single service or function with all dependencies mocked. |
| Integration | test/integration/.*\.integration-spec\.ts$ |
npm run test:integration |
integration-test.yml |
Use for testing module component interactions against a database under test/integration/. |
| E2E | .*\.e2e-spec\.ts$ |
npm run test:e2e |
e2e-test.yml |
Use for full HTTP request/response cycle testing using supertest against a bootstrapped NestJS app. |
Run npm run check-test-match to verify that all test files match one of the three Jest configurations. Spec files matching none of these patterns will fail CI validation.
Unit tests (test/unit/) — Test a single service or function in isolation. Mock all dependencies (Prisma, Stellar SDK, external APIs) using Jest mocks.
// test/unit/escrow.service.spec.ts
describe("EscrowService", () => {
let service: EscrowService;
let mockRepo: jest.Mocked<EscrowRepository>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
EscrowService,
{ provide: EscrowRepository, useValue: mockEscrowRepository() },
{ provide: ContractService, useValue: mockContractService() },
],
}).compile();
service = module.get(EscrowService);
mockRepo = module.get(EscrowRepository);
});
it("throws ConflictException for duplicate escrow", async () => {
mockRepo.findByVendorAndItem.mockResolvedValue(existingEscrow);
await expect(service.createEscrow(dto, vendorAddress)).rejects.toThrow(ConflictException);
});
});Integration tests (test/integration/) — Test a full module with a real test database (in-memory or Docker). Use @nestjs/testing with a real Prisma client pointing to a test DB.
E2E tests (test/*.e2e-spec.ts) — Test full HTTP request-response cycles using supertest. The full NestJS app is bootstrapped against a test DB.
New code must include tests. PRs that drop overall coverage by more than 2% will be asked to add tests before merging.
| Script | Command | Description |
|---|---|---|
| Apply pending migrations | npm run db:migrate |
Runs prisma migrate deploy — applies all pending migrations; safe for CI and production |
| Create a new migration | npx prisma migrate dev --name <name> |
Generates and applies a migration for local development |
| Reset local database | npm run db:reset |
Drops, recreates, re-migrates, and re-seeds the local database (development only — requires confirmation) |
| Populate test data | npm run db:seed |
Runs scripts/db-seed.ts to insert 3 vendor profiles, 10 escrows, 2 disputes, and 10 notifications idempotently |
| Open Prisma Studio | npm run db:studio |
Launches the visual database browser |
| Regenerate Prisma client | npm run db:generate |
Required after editing prisma/schema.prisma |
# First-time setup
npm run db:migrate # apply all migrations
npm run db:seed # populate with test data
# After pulling schema changes
npm run db:generate # regenerate client
npm run db:migrate # apply new migrations
# Start fresh (drops all data)
npm run db:reset # reset + seed in one stepIf your change requires a schema update:
# 1. Edit prisma/schema.prisma
# 2. Generate and apply the migration
npx prisma migrate dev --name describe-your-change
# 3. Regenerate the Prisma client
npx prisma generate
# 4. Commit BOTH the migration files AND schema.prismaMigration rules:
- Never edit existing migration files — create a new one
- Migration names must be descriptive:
add-dispute-resolution-timestamp, notupdate1 - Destructive migrations (dropping columns or tables) must include a comment explaining data handling
- If you're adding a non-nullable column, it must either have a
@defaultvalue or include a migration that backfills existing rows
The CI pipeline uses two levels of caching to reduce run times:
| Cache | Key | What it stores |
|---|---|---|
| npm cache | setup-node cache: 'npm' |
Downloaded npm packages (~/.npm) |
| TypeScript build cache | ts-build-<os>-<hash of tsconfig + src> |
Compiled output in dist/ and tsconfig.build.tsbuildinfo |
The TS build cache is invalidated whenever tsconfig.build.json or any src/**/*.ts file changes. On cache hit, nest build skips unchanged files and only recompiles what changed.
npm ci is used instead of npm install in all CI steps for faster, deterministic installs from the lockfile.
Do not open a public GitHub issue for security vulnerabilities.
This especially applies to:
- The system signer key handling
- The auto-release transaction submission flow
- SEP-10 authentication and JWT verification
- The admin dispute resolution endpoint
Report privately to:
📧 security@trustlink.xyz (or the address in SECURITY.md)
Include:
- Description of the vulnerability
- Steps to reproduce or a proof-of-concept
- Your severity assessment
We aim to acknowledge within 48 hours and patch critical issues within 7 days.
- 💬 Telegram → Join the TrustLink contributor group — fastest way to reach a maintainer
- 💬 GitHub Discussions → Ask a question
- 🐛 GitHub Issues → Confirmed bugs only — include steps to reproduce and error logs
- 🌊 Stellar Developers Discord → discord.gg/stellardev for real-time help
Helpful resources:
- NestJS Documentation
- Prisma Docs
- Stellar Horizon API Reference
- Soroban RPC Reference
- Stellar SEP-10 Specification
- Terminal Africa API Docs
The backend is the quiet engine that makes the promise of trustless commerce actually work. Thank you for helping keep it reliable.