Number Intelligence Service — Local Dev Setup
Version: 1.0 Status: Draft Owner: Messaging Core Last Updated: 2026-04-21 Companion: TESTING_STRATEGY · DEPLOYMENT_TOPOLOGY
1. Prerequisites
| Tool | Version | Purpose |
|---|---|---|
| Node.js | 20 LTS+ | Runtime |
| pnpm | 9+ | Package manager |
| Docker Desktop | 24+ | Postgres, Redis, NATS, mock-MNO-SFTP, mock-SS7-gateway, MinIO, Vault dev |
| grpcurl | latest | gRPC ad-hoc testing |
| psql | 16+ | Direct DB access |
| nats CLI | 0.1+ | Stream/subject inspection |
| mkcert | latest | Local mTLS dev certs (optional) |
| jq | 1.7+ | Pretty-print JSON logs |
| Python | 3.11+ | Audit-chain verifier (parallel impl) |
2. Clone and install
git clone git@github.com:ghasi/sms-gateway.git
cd sms-gateway/services/number-intelligence-service
pnpm install
3. Start infrastructure
The repo provides a service-scoped docker-compose.dev.yml at services/number-intelligence-service/docker-compose.dev.yml:
docker compose -f docker-compose.dev.yml up -d
This starts:
| Container | Host:Port | Notes |
|---|---|---|
| PostgreSQL 16 | localhost:5432 | user ghasi, password ghasi_dev, db ghasi_dev; schema numint |
| Redis 7 (single-node) | localhost:6379 | DB 5 used by service |
| NATS 2.10 (JetStream) | localhost:4222 | Streams pre-created (NUMBER_INTELLIGENCE_EVENTS, NUMINT_RECONCILIATION, NUMINT_EIR, NUMINT_AUDIT_OPS, NUMINT_BILLING) |
| Mock MNO SFTP | localhost:2222 (SFTP) | Serves CSVs from test/fixtures/mno-mnp/ for afghan-wireless, mtn-afghanistan, etisalat-af, roshan |
| Mock ATRA CEIR SFTP | localhost:2223 (SFTP) | Serves blacklist CSV from test/fixtures/atra-ceir/ |
| Mock SS7 gateway | localhost:50074 (gRPC) | Implements LiveLookup proto; deterministic responses driven by test/fixtures/hlr-fixtures.json |
| MinIO | localhost:9000 (S3) | Buckets numint-mnp-raw, numint-eir-raw, numint-hlr-pcap, numint-audit-cold pre-created |
| Vault dev mode | localhost:8200 | Token dev-only-root-token; pre-seeded paths |
The compose file mounts test/fixtures/ into the relevant containers; swap fixtures during testing.
4. Database setup
pnpm prisma migrate dev --name init_numint_schema
pnpm db:seed
The seed script (scripts/seed.ts) populates:
- MNO snapshots for the four major Afghan MNOs (
afghan-wireless,mtn-afghanistan,etisalat-af,roshan,salaam) with prefix tables matching ATRA's published numbering plan. - Sample
number_records— 1 000 MSISDNs in the reserved+9379999XXXXtest range, mixed across MNOs and MNP states. - Sample
portability_history— 50 historical port events. - Sample
eir_records— 100 BLACKLIST + 50 GREYLIST IMEIs (synthetic Luhn-valid). - Sample
tenant_lookup_quotas—t_dev_acmewith default plan (10 RPS, 100 000/month). - Mock platform pepper + tenant salt in Vault dev.
5. Environment variables
Create .env.local in services/number-intelligence-service/:
NODE_ENV=development
LOG_LEVEL=debug
GRPC_PORT=50073
HTTP_PORT=3073
DATABASE_URL=postgresql://ghasi:ghasi_dev@localhost:5432/ghasi_dev?schema=numint
DATABASE_REPLICA_URL=postgresql://ghasi:ghasi_dev@localhost:5432/ghasi_dev?schema=numint
REDIS_URL=redis://localhost:6379/5
NATS_URL=nats://localhost:4222
# Disable mTLS for local dev
GRPC_TLS_ENABLED=false
# Vault dev mode
VAULT_ADDR=http://localhost:8200
VAULT_TOKEN=dev-only-root-token
# Pepper (loaded from Vault in non-dev)
MSISDN_PEPPER=dev-pepper-do-not-use-in-prod
IMEI_PEPPER=dev-imei-pepper
# Mock SS7 gateway
HLR_GATEWAY_ENDPOINT=localhost:50074
# Mock MNO SFTP base
MNO_SFTP_BASE=sftp://test:test@localhost:2222
ATRA_CEIR_SFTP=sftp://test:test@localhost:2223/ceir/blacklist.csv
# MinIO
S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY=minio
S3_SECRET_KEY=minio_dev_pwd
S3_BUCKET_MNP_RAW=numint-mnp-raw
S3_BUCKET_EIR_RAW=numint-eir-raw
S3_BUCKET_HLR_PCAP=numint-hlr-pcap
# Cache
CACHE_WARM_TARGET=10000
LRU_MAX_ENTRIES=10000
# Per-MNO TPS (dev defaults; production loads from operator-management)
TPS_AFGHAN_WIRELESS=100
TPS_MTN=100
TPS_ETISALAT=100
TPS_ROSHAN=100
# Region (for metric labels)
REGION=af-kabul-dev
To enable mTLS locally:
pnpm gen:dev-certs # uses mkcert; output to ./certs/
GRPC_TLS_ENABLED=true
TLS_CERT_PATH=./certs/server.crt
TLS_KEY_PATH=./certs/server.key
TLS_CA_PATH=./certs/ca.crt
6. Run the service
# Hot-path API (gRPC + REST)
pnpm dev
# Batch jobs (in another terminal — disabled by default in dev)
pnpm dev:batch
# Audit verifier (Python; in a third terminal)
cd services/number-intelligence-service/python-verifier
poetry install && poetry run python verifier.py --once
The hot-path service exposes:
- gRPC at
localhost:50073 - REST at
localhost:3073(Swagger UI at/v1/numint/openapi.json) /metrics,/health/ready,/health/live
7. Common dev commands
7.1 Lookup an MSISDN via gRPC
grpcurl -plaintext -d '{"e164":"+93701234567"}' \
localhost:50073 ghasi.sms.numint.v1.NumberIntelligenceService/ResolveMsisdn
7.2 Lookup via REST (Public Lookup API)
# Mint a dev tenant JWT
TOKEN=$(pnpm dev:mint-jwt --tenant t_dev_acme --scope numint:lookup)
curl -H "Authorization: Bearer $TOKEN" -H "X-Tenant-Id: t_dev_acme" \
"http://localhost:3073/v1/lookup/+93701234567?maxStaleness=86400" | jq
7.3 Trigger MNP reconciliation on demand
curl -H "Authorization: Bearer $ADMIN_TOKEN" -X POST \
-d '{"mnoId":"mtn-afghanistan","fileUrl":"sftp://test:test@localhost:2222/mnp/2026-04-20.csv"}' \
http://localhost:3073/v1/admin/numint/mnp/runs
7.4 Force a live HLR probe
grpcurl -plaintext -d '{"e164":"+93701234567","tps_wait_ms":500}' \
localhost:50073 ghasi.sms.numint.v1.NumberIntelligenceService/ProbeHlr
7.5 Test tenant quota enforcement
# Hit the API rapidly; expect 429 after the configured RPS
for i in $(seq 1 50); do
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: t_dev_acme" "http://localhost:3073/v1/lookup/+93701234567"
done
7.6 Inspect outbox / NATS
psql $DATABASE_URL -c "SELECT subject, count(*) FROM numint.outbox WHERE published_at IS NULL GROUP BY subject"
nats stream report
nats consumer info NUMBER_INTELLIGENCE_EVENTS
7.7 Verify audit chain manually
pnpm chain:verify --table lookup_audit --partition lookup_audit_2026_04
pnpm chain:verify --table portability_history --mno mtn-afghanistan
7.8 Trigger MNP conflict for testing
The seed includes a fixture that lists +93701555000 as ported to both afghan-wireless and mtn-afghanistan on consecutive days.
# Run both MNOs' reconciliations
curl -X POST .../v1/admin/numint/mnp/runs -d '{"mnoId":"afghan-wireless","fileUrl":"sftp://.../afghan-wireless-conflict.csv"}'
curl -X POST .../v1/admin/numint/mnp/runs -d '{"mnoId":"mtn-afghanistan","fileUrl":"sftp://.../mtn-conflict.csv"}'
# Inspect the conflict
curl http://localhost:3073/v1/admin/numint/mnp/conflicts | jq
# Resolve
curl -X POST .../v1/admin/numint/mnp/conflicts/cfl_xxx/resolve \
-d '{"resolution":"B_WINS","note":"MTN file dated later"}'
8. Running tests
# Unit
pnpm test
# Integration (Testcontainers — slower)
pnpm test:integration
# Contract (Pact)
pnpm test:contract
# Load test (k6 must be installed separately)
pnpm test:load:hot-path
# Property-based (fast-check)
pnpm test:prop
# Coverage
pnpm test:coverage
Coverage reports land in coverage/ and lcov.info. Per-area thresholds enforced in jest.config.ts.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
pnpm dev fails with "PEPPER_VERSION mismatch" | Vault dev seed not run | pnpm vault:seed |
LookupEir returns INVALID_ARGUMENT for valid IMEI | Luhn fixture wrong | Verify with pnpm imei:gen |
| MNP recon job hangs | Mock SFTP container not running | docker compose ps; restart |
| Prefix-table fallback returning UNKNOWN for AF MSISDNs | ATRA CSV not seeded into Vault | pnpm vault:seed:prefix-table |
| Live HLR probe returns ADAPTER_DOWN | Mock SS7 gateway crashed | docker compose logs mock-ss7; restart |
| Outbox not draining | NATS not connected | Check NATS_URL; nats server check connection |
chain:verify reports BROKEN | Pepper rotated mid-run; or genuine bug | Confirm pepper_version consistency; if genuine, file an issue |
10. Tearing down
docker compose -f docker-compose.dev.yml down -v # -v wipes volumes
11. Tips for productive local dev
- Watch logs with jq:
pnpm dev | jq -c 'select(.event | startswith("numint.lookup"))'to see only lookup events. - Pin a single-pod scenario for cache testing: the LRU is in-process; multi-pod local runs would not exercise the same LRU instance. Use
pnpm dev:singlefor deterministic LRU behaviour. - Hot-reload prefix table:
curl -X POST http://localhost:3073/v1/admin/numint/internal/reload-prefix-table(dev-only endpoint). - Enable verbose SS7 PCAP capture: set
HLR_PCAP_SAMPLE_RATE=1.0in.env.localto capture every probe locally for debugging.