Skip to content

Commit d0db963

Browse files
authored
Merge branch 'staging' into feat/wallet-archive-shutdown-redaction
2 parents b9073af + a657016 commit d0db963

62 files changed

Lines changed: 3843 additions & 54 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ WALLET_ENCRYPTION_KEY=your-secret-encryption-key-min-32-chars
3131
# Mainnet: https://horizon.stellar.org
3232
# ------------------------------------------------------------
3333
STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
34+
STELLAR_NETWORK=TESTNET
35+
STELLAR_HORIZON_MAX_RETRIES=3
36+
STELLAR_HORIZON_RETRY_BACKOFF_MS=500
37+
STELLAR_HORIZON_RETRY_JITTER_MS=250
38+
BALANCE_STALE_THRESHOLD_MS=300000 # 5 minutes
39+
# Webhook Configuration
3440

3541
# ------------------------------------------------------------
3642
# Balance Indexer

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ It handles wallet creation, transaction orchestration, fee sponsorship, and on-c
3333

3434
## API Endpoints
3535

36+
All routes below are served under the `/v1` prefix (e.g. `GET /v1/health`). See [docs/API-VERSIONING.md](docs/API-VERSIONING.md) for the versioning strategy.
37+
3638
### Health & Monitoring
3739

3840
#### `GET /health`

docs/API-VERSIONING.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# API Versioning Strategy
2+
3+
This document describes how the Mux backend versions its public HTTP API.
4+
5+
## Current approach: URI path versioning
6+
7+
All routes are served under a global `/v1` prefix, applied once in `src/main.ts`:
8+
9+
```ts
10+
app.setGlobalPrefix('v1');
11+
```
12+
13+
Individual controllers (e.g. `@Controller('auth')`, `@Controller('wallets')`)
14+
declare their resource path only; the version prefix is applied globally so
15+
every route is automatically namespaced (`/v1/auth/authenticate`,
16+
`/v1/wallets`, etc.). Requests made without the `/v1` prefix return `404 Not
17+
Found` — there is no unversioned fallback.
18+
19+
## Why URI versioning
20+
21+
- **Explicit and cache-friendly**: the version is visible in the URL, in logs,
22+
and in reverse-proxy/CDN routing rules, without relying on a header that
23+
intermediaries may strip.
24+
- **Simple for consumers**: partners and the frontend hard-code a base URL
25+
(e.g. `https://api.mux.dev/v1`) rather than needing to set a custom header
26+
on every request.
27+
- **Matches existing NestJS conventions** in this repo — a single
28+
`setGlobalPrefix` call versions every controller without per-route
29+
decorators.
30+
31+
## Introducing a breaking change (`/v2`)
32+
33+
When a change is not backwards compatible:
34+
35+
1. Add the new/changed controllers under a `v2` path (NestJS's built-in
36+
[URI versioning](https://docs.nestjs.com/techniques/versioning) via
37+
`app.enableVersioning({ type: VersioningType.URI })` can be adopted at that
38+
point to run `v1` and `v2` controllers side by side).
39+
2. Keep `/v1` serving the previous behavior until consumers have migrated.
40+
3. Announce the deprecation window for `/v1` in release notes before removal.
41+
42+
## Non-breaking changes
43+
44+
Additive changes (new endpoints, new optional request/response fields) ship
45+
directly under the current `/v1` prefix — no new version is required.
46+
47+
## Health and monitoring endpoints
48+
49+
`/v1/health` and `/v1/ready` follow the same prefix as every other route, so
50+
uptime checks and readiness probes must be configured with the `/v1` path.

docs/migration-recovery-runbook.md

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# Failed Migration Recovery Runbook
2+
3+
## Overview
4+
5+
This runbook provides procedures for detecting, diagnosing, and recovering from failed database migrations in the Mux Backend API.
6+
7+
## Quick Reference
8+
9+
| Scenario | Steps | Recovery Time |
10+
|----------|-------|---------------|
11+
| Migration hangs | Check logs → Kill process → Rollback | 5-10 min |
12+
| Syntax error | Fix schema → Rollback → Retry | 10-15 min |
13+
| Constraint violation | Backfill data → Rollback → Retry | 15-30 min |
14+
| Lock timeout | Kill blocking query → Retry | 5 min |
15+
16+
---
17+
18+
## Detection
19+
20+
### Signs of Migration Failure
21+
22+
1. **Application startup fails** with migration error
23+
2. **Database logs** show:
24+
- `ERROR: relation "table_name" already exists`
25+
- `ERROR: column "column_name" does not exist`
26+
- `deadlock detected`
27+
- `statement timeout`
28+
3. **Metrics** show stuck migration:
29+
- Long-running transaction in `pg_stat_activity`
30+
- No progress on migration commit
31+
32+
### Check Migration Status
33+
34+
```bash
35+
# List applied migrations
36+
psql -U $DB_USER -d $DB_NAME -c "SELECT * FROM _prisma_migrations ORDER BY finished_at DESC LIMIT 10;"
37+
38+
# Find stuck migrations
39+
psql -U $DB_USER -d $DB_NAME -c "SELECT * FROM _prisma_migrations WHERE finished_at IS NULL;"
40+
41+
# Check long-running transactions
42+
psql -U $DB_USER -d $DB_NAME -c "SELECT * FROM pg_stat_activity WHERE state = 'active' AND xact_start < NOW() - INTERVAL '5 minutes';"
43+
```
44+
45+
---
46+
47+
## Recovery Procedures
48+
49+
### Scenario 1: Syntax Error in Migration
50+
51+
**Symptoms:**
52+
- `ERROR: syntax error at or near...`
53+
- Migration marked as started but not finished
54+
55+
**Steps:**
56+
57+
1. **Stop the application**
58+
```bash
59+
kubectl scale deployment mux-api --replicas=0
60+
```
61+
62+
2. **Identify the failed migration**
63+
```bash
64+
psql -U $DB_USER -d $DB_NAME -c "SELECT name FROM _prisma_migrations WHERE finished_at IS NULL;"
65+
```
66+
67+
3. **Rollback (Prisma handles this)**
68+
```bash
69+
# Prisma automatically rolls back failed migrations
70+
npm run prisma:migrate:resolve -- --rolled-back <migration-name>
71+
```
72+
73+
4. **Fix the migration file**
74+
- Edit the migration SQL in `prisma/migrations/<timestamp>_<name>/migration.sql`
75+
- Correct syntax errors
76+
77+
5. **Retry migration**
78+
```bash
79+
npm run prisma:migrate:deploy
80+
```
81+
82+
6. **Restart application**
83+
```bash
84+
kubectl scale deployment mux-api --replicas=3
85+
```
86+
87+
### Scenario 2: Constraint Violation
88+
89+
**Symptoms:**
90+
- `ERROR: duplicate key value violates unique constraint`
91+
- `ERROR: insert or update on table violates foreign key constraint`
92+
93+
**Steps:**
94+
95+
1. **Analyze constraint violation**
96+
```bash
97+
psql -U $DB_USER -d $DB_NAME -c "SELECT * FROM table_name WHERE condition;"
98+
```
99+
100+
2. **Fix conflicting data** (backfill or cleanup)
101+
```sql
102+
-- Example: Remove duplicates before adding UNIQUE constraint
103+
DELETE FROM table_name WHERE id NOT IN (
104+
SELECT MIN(id) FROM table_name GROUP BY unique_col
105+
);
106+
```
107+
108+
3. **Rollback migration**
109+
```bash
110+
npm run prisma:migrate:resolve -- --rolled-back <migration-name>
111+
```
112+
113+
4. **Retry after data fix**
114+
```bash
115+
npm run prisma:migrate:deploy
116+
```
117+
118+
### Scenario 3: Lock Timeout
119+
120+
**Symptoms:**
121+
- `ERROR: canceling statement due to lock timeout`
122+
- `statement timeout` in logs
123+
124+
**Steps:**
125+
126+
1. **Identify blocking queries**
127+
```bash
128+
psql -U $DB_USER -d $DB_NAME -c "SELECT blocked_locks.pid, blocked_locks.relation::regclass, blocking_locks.pid, blocking_locks.relation::regclass FROM pg_locks blocked_locks JOIN pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocking_locks.granted AND NOT blocked_locks.granted WHERE NOT blocked_locks.granted;"
129+
```
130+
131+
2. **Terminate blocking transaction**
132+
```bash
133+
psql -U $DB_USER -d $DB_NAME -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid != pg_backend_pid() AND query LIKE '%your-table-name%' AND state = 'active';"
134+
```
135+
136+
3. **Increase lock_timeout** (temporary)
137+
```sql
138+
SET lock_timeout = '30 seconds';
139+
```
140+
141+
4. **Retry migration**
142+
```bash
143+
npm run prisma:migrate:deploy
144+
```
145+
146+
### Scenario 4: Hung Migration
147+
148+
**Symptoms:**
149+
- Migration started hours ago
150+
- No errors in logs
151+
- Application waiting on migration
152+
153+
**Steps:**
154+
155+
1. **Check migration status**
156+
```bash
157+
psql -U $DB_USER -d $DB_NAME -c "SELECT * FROM _prisma_migrations WHERE finished_at IS NULL AND started_at < NOW() - INTERVAL '1 hour';"
158+
```
159+
160+
2. **Identify long-running transaction**
161+
```bash
162+
psql -U $DB_USER -d $DB_NAME -c "SELECT pid, usename, xact_start, state_change, query FROM pg_stat_activity WHERE xact_start < NOW() - INTERVAL '1 hour';"
163+
```
164+
165+
3. **Terminate stuck transaction**
166+
```bash
167+
psql -U $DB_USER -d $DB_NAME -c "SELECT pg_terminate_backend(<pid>);"
168+
```
169+
170+
4. **Mark migration as rolled back**
171+
```bash
172+
npm run prisma:migrate:resolve -- --rolled-back <migration-name>
173+
```
174+
175+
5. **Investigate root cause** before retry
176+
- Check for missing indexes
177+
- Verify disk space
178+
- Review lock contention
179+
180+
---
181+
182+
## Verification
183+
184+
### After Any Recovery Attempt
185+
186+
1. **Verify database consistency**
187+
```bash
188+
npm run prisma:generate
189+
npm run prisma:migrate:status
190+
```
191+
192+
2. **Run integrity checks**
193+
```bash
194+
npm run db:integrity-check
195+
```
196+
197+
3. **Test critical flows**
198+
```bash
199+
npm run test:integration -- --suite=payments
200+
npm run test:integration -- --suite=wallets
201+
npm run test:integration -- --suite=recovery
202+
```
203+
204+
4. **Monitor application health**
205+
```bash
206+
kubectl logs -f deployment/mux-api -c mux-api | grep -E "ERROR|WARN|migration"
207+
```
208+
209+
---
210+
211+
## Prevention
212+
213+
### Best Practices
214+
215+
1. **Test migrations locally first**
216+
```bash
217+
docker-compose up -d postgres
218+
npm run prisma:migrate:dev
219+
```
220+
221+
2. **Write idempotent migrations**
222+
- Use `IF NOT EXISTS` / `IF EXISTS`
223+
- Handle both old and new schema during transition
224+
225+
3. **Add data backfill migrations separately**
226+
- Split schema changes and data changes
227+
- Allows rollback at schema layer
228+
229+
4. **Monitor lock timeouts**
230+
- Set `statement_timeout = 30s` for large ALTER TABLE
231+
- Use `ALTER TABLE ... CONCURRENTLY` for indexes on large tables
232+
233+
5. **Use feature flags for compatibility**
234+
- Support both old and new column names during migration
235+
- Clean up old code after deployment
236+
237+
### Example: Safe Schema Evolution
238+
239+
```sql
240+
-- Migration 1: Add new column
241+
ALTER TABLE payments ADD COLUMN assetCode TEXT;
242+
243+
-- Migration 2: Populate data (separate, can be retried safely)
244+
UPDATE payments SET assetCode = currency WHERE assetCode IS NULL;
245+
246+
-- Migration 3: Add constraints
247+
ALTER TABLE payments ALTER COLUMN assetCode SET NOT NULL;
248+
249+
-- Migration 4: Deprecate old column (after code updated)
250+
-- ALTER TABLE payments DROP COLUMN currency_old;
251+
```
252+
253+
---
254+
255+
## Troubleshooting
256+
257+
| Error | Cause | Fix |
258+
|-------|-------|-----|
259+
| `relation already exists` | Migration already applied | Check `_prisma_migrations` table, mark as rolled-back |
260+
| `column does not exist` | Schema mismatch | Regenerate Prisma client: `npm run prisma:generate` |
261+
| `deadlock detected` | Concurrent migrations | Ensure migrations run serially, check app replicas |
262+
| `statement timeout` | Large table operation | Increase timeout or break into smaller batches |
263+
| `disk space low` | Insufficient storage | Add disk space or clean old transaction logs |
264+
265+
---
266+
267+
## Escalation
268+
269+
**Immediate:**
270+
- Migration stuck > 30 minutes
271+
- Multiple rollback failures
272+
- Production data corruption suspected
273+
274+
**Contact:**
275+
- On-call DBA: `@dba-oncall` (Slack)
276+
- Database team: `database-team@mux-labs.com`
277+
- CTO: For critical data loss scenarios
278+
279+
---
280+
281+
## Audit & Compliance
282+
283+
All failed migrations are tracked via `MigrationRecoveryService`:
284+
- Logged to application logs
285+
- Recovery actions recorded in service state
286+
- Use for post-incident analysis
287+
288+
**Retention:** 30 days in recovery service memory (logs permanent in ELK)
289+
290+
---
291+
292+
## Related Documentation
293+
294+
- [Prisma Migrations Guide](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate)
295+
- [PostgreSQL Transaction Handling](https://www.postgresql.org/docs/current/runtime-config-client.html)
296+
- [Mux Backend Architecture](../docs/architecture.md)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- Migration: add assetCode field to Payment
2+
--
3+
-- assetCode is an optional field that stores ISO 4217 currency code or custom asset identifier.
4+
-- Used to validate and track which asset is being transferred in a payment.
5+
6+
ALTER TABLE "Payment" ADD COLUMN "assetCode" TEXT;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable: add defaultNetwork preference to User
2+
ALTER TABLE "User" ADD COLUMN "defaultNetwork" "WalletNetwork";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Migration: add lastLoginIp and lastLoginUserAgent to User
2+
--
3+
-- Captures the IP address and User-Agent seen on the user's most recent
4+
-- successful authentication, alongside the existing lastLoginAt timestamp.
5+
-- Both columns are nullable so existing rows require no backfill.
6+
7+
ALTER TABLE "User" ADD COLUMN "lastLoginIp" TEXT;
8+
ALTER TABLE "User" ADD COLUMN "lastLoginUserAgent" TEXT;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- AlterTable
2+
ALTER TABLE "Payment" ADD COLUMN "idempotencyKey" TEXT;
3+
4+
-- CreateIndex
5+
CREATE UNIQUE INDEX "Payment_idempotencyKey_key" ON "Payment"("idempotencyKey");
6+
7+
-- CreateIndex
8+
CREATE INDEX "Payment_idempotencyKey_idx" ON "Payment"("idempotencyKey");

0 commit comments

Comments
 (0)