Milestones in TalentTrust are a first-class field on the Contract resource. There are no standalone milestone endpoints — all milestone operations flow through the contracts API.
Cross-references: src/routes/contracts.routes.ts, src/modules/contracts/dto/contract.dto.ts, src/contracts/bounds.ts
- Milestone Object Shape
- Policy Bounds
- Feature Flag: MILESTONES_ENABLED
- Endpoints
- Error Codes
- Authentication & Permissions
- Idempotency
{
"title": "string (1-100 chars, required)",
"description": "string (1-500 chars, defaults to '' on create)",
"amount": "number (positive, required)",
"deadline": "ISO 8601 datetime string (optional)",
"completed": "boolean (defaults to false)"
}| Field | Type | Required | Constraints | Source |
|---|---|---|---|---|
title |
string |
Yes | 1–100 characters | contract.dto.ts:16 |
description |
string |
Create: no (default ''), Update: yes |
1–500 characters | contract.dto.ts:17 |
amount |
number |
Yes | Positive (> 0) |
contract.dto.ts:18 |
deadline |
string |
No | Valid ISO 8601 datetime | contract.dto.ts:19 |
completed |
boolean |
No | Defaults to false |
contract.dto.ts:20 |
Defined in src/contracts/bounds.ts:9-10:
| Constant | Value | Description |
|---|---|---|
MAX_MILESTONES_PER_CONTRACT |
20 |
Maximum number of milestones per contract |
MAX_CONTRACT_AMOUNT_STROOPS |
100,000,000,000,000 |
Maximum contract amount in stroops (10,000,000 XLM) |
Additional business rule (enforced in contracts.service.ts:71-81):
- The sum of all milestone
amountvalues must not exceed the contract'sbudget.
The milestones feature is controlled by the MILESTONES_ENABLED environment variable. This allows the feature to be toggled at runtime without a deploy.
| Variable | Type | Default | Description |
|---|---|---|---|
MILESTONES_ENABLED |
boolean |
true |
Enables or disables milestones behaviour at runtime. |
Milestones flow through the contracts API as normal:
- Milestone fields supplied in
POST /api/v1/contractsandPATCH /api/v1/contracts/:idare validated and enforced. - Milestone count is capped at
MAX_MILESTONES_PER_CONTRACT(20). - The sum of milestone amounts must not exceed the contract
budget. - Violations return
422 contract_bounds_error.
Milestone support is fully disabled without a deploy:
- Any
milestonesfield supplied in a request body is silently stripped before reaching the service layer. - No milestone validation (bounds checks, budget cap) is performed.
- The contract is created or updated as if no milestones were included in the request.
- Callers receive a successful response — the strip is transparent.
Set the flag in your environment or .env file:
# Disable milestones (e.g. during a feature rollout pause)
MILESTONES_ENABLED=false
# Re-enable (or omit entirely — defaults to enabled)
MILESTONES_ENABLED=trueAccepted values (case-insensitive): true, false, 1, 0.
| Concern | File |
|---|---|
| Flag declaration (Zod schema) | src/config/env.schema.ts |
Flag in AppConfig / loadConfig |
src/appConfiguration.ts |
| Service-layer enforcement | src/services/contracts.service.ts |
| Flag unit tests | src/services/milestones.flag.test.ts |
POST /api/v1/contracts
Creates a new contract with optional milestones.
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer <JWT> |
Idempotency-Key |
Yes | UUID for idempotent creation |
{
"title": "Website Redesign",
"description": "Complete redesign of the company website",
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"freelancerId": "660e8400-e29b-41d4-a716-446655440001",
"budget": 5000000,
"deadline": "2026-12-31T23:59:59.000Z",
"status": "draft",
"milestones": [
{
"title": "Design Mockups",
"description": "Create wireframes and high-fidelity mockups",
"amount": 1500000,
"deadline": "2026-09-30T23:59:59.000Z",
"completed": false
},
{
"title": "Frontend Implementation",
"description": "Implement responsive frontend based on approved mockups",
"amount": 2000000,
"deadline": "2026-11-30T23:59:59.000Z",
"completed": false
},
{
"title": "Final Delivery",
"description": "Deploy to production and handoff documentation",
"amount": 1500000,
"deadline": "2026-12-31T23:59:59.000Z",
"completed": false
}
]
}| Field | Type | Constraints |
|---|---|---|
title |
string |
5–100 characters |
description |
string |
10–1000 characters |
clientId |
string |
Valid UUID |
budget |
number |
Positive, ≤ MAX_CONTRACT_AMOUNT_STROOPS |
freelancerId |
string |
Valid UUID (optional) |
deadline |
string |
ISO 8601 datetime (optional) |
status |
string |
draft, active, completed, cancelled, or disputed (optional) |
terms |
string |
Free text (optional) |
{
"status": "success",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"title": "Website Redesign",
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"freelancerId": "660e8400-e29b-41d4-a716-446655440001",
"amount": 5000000,
"status": "draft",
"createdAt": "2026-07-24T10:00:00.000Z",
"version": 0
},
"requestId": "req-abc123"
}Note: Milestones are validated at the API layer but are not persisted in the database response. They are enforced for bounds checking (count and total amount) during creation.
| Status | Code | Trigger |
|---|---|---|
400 |
validation_error |
Invalid body fields (Zod validation) |
400 |
bad_request |
Missing Idempotency-Key header |
401 |
unauthorized |
Invalid or missing JWT |
409 |
conflict |
Reused Idempotency-Key with different body |
422 |
contract_bounds_error |
Milestone count > 20, or total amount > budget/cap |
PATCH /api/v1/contracts/:id
Updates an existing contract. Supports partial updates via Optimistic Concurrency Control (OCC).
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer <JWT> |
| Param | Type | Description |
|---|---|---|
id |
string (UUID) |
Contract ID |
{
"version": 0,
"milestones": [
{
"title": "Design Mockups",
"description": "Create wireframes and high-fidelity mockups - REVISED",
"amount": 1800000,
"deadline": "2026-10-15T23:59:59.000Z",
"completed": true
},
{
"title": "Frontend Implementation",
"description": "Implement responsive frontend based on approved mockups",
"amount": 2000000,
"deadline": "2026-11-30T23:59:59.000Z",
"completed": false
},
{
"title": "Final Delivery",
"description": "Deploy to production and handoff documentation",
"amount": 1200000,
"deadline": "2026-12-31T23:59:59.000Z",
"completed": false
}
]
}| Field | Type | Description |
|---|---|---|
version |
integer ≥ 0 |
OCC version from last read (required) |
All contract fields are optional for PATCH. When milestones is provided, it replaces the entire milestones array.
{
"status": "success",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"title": "Website Redesign",
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"freelancerId": "660e8400-e29b-41d4-a716-446655440001",
"amount": 5000000,
"status": "draft",
"createdAt": "2026-07-24T10:00:00.000Z",
"version": 1
},
"requestId": "req-def456"
}| Status | Code | Trigger |
|---|---|---|
400 |
validation_error |
Invalid body fields (Zod validation) |
400 |
ERR_MISSING_VERSION |
Missing version field |
400 |
ERR_INVALID_VERSION |
version is not a non-negative integer |
401 |
unauthorized |
Invalid or missing JWT |
404 |
not_found |
Contract not found |
409 |
ERR_CONFLICT |
Version conflict (stale OCC version) |
422 |
contract_bounds_error |
Milestone count > 20, or total amount > budget/cap |
GET /api/v1/contracts/:id
Fetches a single contract by ID.
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer <JWT> |
| Param | Type | Description |
|---|---|---|
id |
string (UUID) |
Contract ID |
{
"status": "success",
"data": {
"id": "770e8400-e29b-41d4-a716-446655440002",
"title": "Website Redesign",
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"freelancerId": "660e8400-e29b-41d4-a716-446655440001",
"amount": 5000000,
"status": "draft",
"createdAt": "2026-07-24T10:00:00.000Z",
"version": 0
},
"requestId": "req-ghi789"
}| Status | Code | Trigger |
|---|---|---|
401 |
unauthorized |
Invalid or missing JWT |
404 |
not_found |
Contract not found |
GET /api/v1/contracts/bounds
Returns the enforced per-contract limits for client discovery.
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer <JWT> |
{
"status": "success",
"data": {
"maxMilestonesPerContract": 20,
"maxContractAmountStroops": 100000000000000
},
"requestId": "req-jkl012"
}| Status | Code | Trigger |
|---|---|---|
401 |
unauthorized |
Invalid or missing JWT |
{
"status": "error",
"error": {
"code": "contract_bounds_error",
"message": "Milestone count 21 exceeds maximum of 20",
"requestId": "req-abc123"
}
}{
"status": "error",
"error": {
"code": "validation_error",
"message": "Request validation failed",
"requestId": "req-abc123",
"details": [
{
"path": ["body", "milestones", "0", "title"],
"message": "String must contain at least 1 character(s)",
"code": "too_short"
}
]
}
}| HTTP Status | Code | Message | Source |
|---|---|---|---|
400 |
validation_error |
Request validation failed | validate.middleware.ts:46-51 |
400 |
bad_request |
Idempotency-Key header is required | contractIdempotency.ts:67-74 |
400 |
ERR_MISSING_VERSION |
version field is required for updates | appError.ts:80-82 |
400 |
ERR_INVALID_VERSION |
version must be a non-negative integer | appError.ts:85-88 |
401 |
unauthorized |
Authentication is required | contractIdempotency.ts:80-88 |
404 |
not_found |
The requested resource was not found | appError.ts:67-70 |
409 |
ERR_CONFLICT |
Version conflict | appError.ts:91-94 |
409 |
conflict |
Idempotency-Key was reused with a different request body | contractIdempotency.ts:97-104 |
422 |
contract_bounds_error |
Milestone count N exceeds maximum of 20 | bounds.ts:63 |
422 |
contract_bounds_error |
Total milestone amount exceeds maximum contract amount of 100000000000000 stroops | bounds.ts:73 |
422 |
contract_bounds_error |
Total milestone amount exceeds maximum contract amount (milestones total N exceeds budget of M) | contracts.service.ts:78-79 |
500 |
internal_error |
An unexpected error occurred | appError.ts:193-202 |
All milestones endpoints require a valid JWT Bearer token.
| Role | Create | Read | Update | Delete | List |
|---|---|---|---|---|---|
admin |
ALLOW | ALLOW | ALLOW | ALLOW | ALLOW |
client |
ALLOW | OWN | OWN | DENY | OWN |
freelancer |
DENY | OWN | OWN | DENY | OWN |
auditor |
DENY | ALLOW | DENY | DENY | ALLOW |
OWN = Only if the authenticated user's ID matches the contract's clientId (verified via getContractOwnerId DB lookup).
{
"sub": "user-id",
"email": "user@example.com",
"role": "client",
"iat": 1690000000,
"exp": 1690003600
}Algorithm: HS256.
Contract creation (POST /api/v1/contracts) requires an Idempotency-Key header. Keys are scoped to the authenticated user.
| Scenario | Response |
|---|---|
| First request with key | Process normally, cache response |
| Replay with same key + same body | Return cached response with Idempotency-Replayed: true header |
| Replay with same key + different body | 409 Conflict |
| Missing key | 400 Bad Request |
Source: src/middleware/contractIdempotency.ts
All PATCH requests must include a version field. The update succeeds only if the stored version matches the supplied value; the version is then atomically incremented. If another writer updated the contract first, a 409 Version Conflict error is returned.
Flow:
GET /api/v1/contracts/:id→ returns currentversionPATCH /api/v1/contracts/:idwithversionfrom step 1- If version matches → update succeeds, version incremented
- If version differs →
409 ERR_CONFLICT
Source: src/services/contracts.service.ts:130-167
Milestones support soft-delete so deletes are reversible within a retention window.
| Concern | Behaviour |
|---|---|
| Soft-delete | DELETE /api/v1/contracts/:id/milestones/:milestoneId sets deletedAt (record is kept) |
| Default reads | GET /api/v1/contracts/:id/milestones excludes soft-deleted rows (?includeDeleted=true to include) |
| Restore | POST /api/v1/contracts/:id/milestones/:milestoneId/restore clears deletedAt if still inside the retention window; otherwise 410 soft_delete_retention_expired |
| Retention | MILESTONES_SOFT_DELETE_RETENTION_DAYS (default 30) |
| Purge | runMilestonesSoftDeletePurge() hard-deletes soft-deleted rows past the window (maintenance / cron) |
Sources: src/services/milestones.service.ts, src/controllers/milestones.softdelete.controller.ts, src/utils/softDelete.ts