Skip to main content

sender-id-registry-service — Local Development Setup

Version: 1.0 Status: Draft Owner: Trust & Safety + Platform Engineering Last Updated: 2026-04-21 Companion: DEPLOYMENT_TOPOLOGY · TESTING_STRATEGY


1. Prerequisites

ToolVersionPurpose
Node.js20 LTS+Runtime
pnpm9+Package manager (monorepo workspace)
Docker Desktop24+Postgres, Redis, NATS, MinIO, mocks
grpcurllatestgRPC testing
mkcertlatestLocal mTLS certs
dnsmasq (optional)latestLocal DNS test server

2. Clone and Install

git clone git@github.com:ghasi/sms-gateway.git
cd sms-gateway/services/sender-id-registry-service
pnpm install

3. Start Infrastructure

The monorepo's shared docker-compose includes a sender-id-registry profile:

# From repo root
docker compose --profile sid up -d

This starts:

  • PostgreSQL on localhost:5432 (user: ghasi, password: ghasi_dev, schema: sender_id_registry)
  • Redis on localhost:6379 (DB 5 used by this service)
  • NATS JetStream on localhost:4222
  • MinIO (S3-compatible) on localhost:9000 with buckets ghasi-sid-kyc-local and ghasi-sid-regulator-export-local
  • SoftHSM2 sidecar on localhost:5657 (for regulator-export signing in dev)
  • mock-dns-server on localhost:8053 (returns canned TXT records for DNS-TXT verification testing)
  • mock-regulator-sftp on localhost:2222 (logs received files to ./tmp/atra-mock/)
  • mock-channel-router on localhost:50061 (records OTP requests to ./tmp/otp-log.jsonl)

Compose snippet (services/sender-id-registry-service/docker-compose.yml)

services:
sid-postgres:
image: postgres:15
environment:
POSTGRES_USER: ghasi
POSTGRES_PASSWORD: ghasi_dev
POSTGRES_DB: ghasi_dev
ports: ["5432:5432"]

sid-redis:
image: redis:7-alpine
ports: ["6379:6379"]

sid-nats:
image: nats:2.10
command: -js -m 8222
ports: ["4222:4222", "8222:8222"]

sid-minio:
image: minio/minio:latest
environment:
MINIO_ROOT_USER: ghasidev
MINIO_ROOT_PASSWORD: ghasidevsecret
command: server /data --console-address ":9001"
ports: ["9000:9000", "9001:9001"]
volumes: [./tmp/minio-data:/data]

sid-softhsm:
image: softhsm/softhsm2:latest
environment:
SOFTHSM2_PIN: 1234
SOFTHSM2_SOPIN: 5678
ports: ["5657:5657"]

sid-mock-dns:
image: ghcr.io/ghasi/mock-dns-server:latest
ports: ["8053:53/udp", "8053:53/tcp"]
volumes: [./test/mock-dns/zones.yaml:/etc/zones.yaml]

sid-mock-regulator-sftp:
image: atmoz/sftp:latest
command: regulator:regulator:1001
ports: ["2222:22"]
volumes: [./tmp/atra-mock:/home/regulator/incoming]

sid-mock-channel-router:
image: ghcr.io/ghasi/mock-channel-router:latest
ports: ["50061:50061"]
volumes: [./tmp/otp-log.jsonl:/var/log/otp.jsonl]

4. Database Setup

# From services/sender-id-registry-service
pnpm db:migrate # apply migrations to create sender_id_registry schema
pnpm db:seed # seed restricted patterns + sample sender-IDs

The seed script populates:

  • All default restricted_patterns (BANK*, GOV*, MNO*, JUDICIAL, HEALTH, EMERGENCY)
  • 3 sample sender-IDs in different states:
    • sid_dev_active_bank: BANK-DEV ALPHA, ACTIVE, NOTARISED + DOMAIN_DNS
    • sid_dev_pending: RETAIL-DEV ALPHA, KYC_REVIEW
    • sid_dev_suspended: LOWREP-DEV ALPHA, SUSPENDED (low reputation)
  • 6 sample KYC documents in MinIO bucket
  • 1 sample regulator export

5. Environment Variables

Create .env.local in services/sender-id-registry-service/:

NODE_ENV=development
LOG_LEVEL=debug
GRPC_PORT=50091
HTTP_PORT=3091
REGION=local

DATABASE_URL=postgresql://ghasi:ghasi_dev@localhost:5432/ghasi_dev?schema=sender_id_registry
REDIS_URL=redis://localhost:6379/5
NATS_URL=nats://localhost:4222
NATS_CREDS_PATH=./test/nats.creds.dev

S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY=ghasidev
S3_SECRET_KEY=ghasidevsecret
S3_KYC_BUCKET=ghasi-sid-kyc-local
S3_REGULATOR_EXPORT_BUCKET=ghasi-sid-regulator-export-local

# Disable mTLS in local dev
GRPC_TLS_ENABLED=false

# SoftHSM2 for regulator-export signing
HSM_PKCS11_LIB=/usr/lib/softhsm/libsofthsm2.so
HSM_REGULATOR_EXPORT_KEY_SLOT=0
HSM_PIN=1234

# Vault disabled in dev — use file-based keys
VAULT_ADDR=disabled
KYC_KEK_DEV_KEY=./test/keys/dev-kek.key

# Mock external services
ATRA_SFTP_HOST=localhost
ATRA_SFTP_PORT=2222
ATRA_SFTP_USER=regulator
ATRA_SFTP_KEY=./test/keys/regulator.key

DNS_RESOLVER_PRIMARY=127.0.0.1:8053
DNS_RESOLVER_SECONDARY=127.0.0.1:8053

# AI mock
AI_LLM_URL=mock
AI_OCR_URL=mock

# Channel router
CHANNEL_ROUTER_GRPC_URL=localhost:50061

OTP_TTL_SECONDS=60 # shorter for dev convenience
OTP_RATE_LIMIT_PER_HOUR=100

6. Run the Service

# Development with hot reload
pnpm start:dev

# Or run in debug mode
pnpm start:debug

Service logs appear in terminal. Health endpoints:

  • http://localhost:3091/health/live
  • http://localhost:3091/health/ready
  • http://localhost:3091/metrics

7. Test the gRPC Endpoint

grpcurl -plaintext \
-proto src/proto/sid.proto \
-d '{
"sender_id": "BANK-DEV",
"type": "ALPHA",
"tenant_id": "00000000-0000-4000-8000-000000000001",
"trace_id": "local-test-1"
}' \
localhost:50091 \
ghasi.sms.sid.v1.SenderIdRegistryService/Verify

Expected:

{
"status": "ACTIVE",
"currentLevel": "NOTARISED",
"hasDomainDns": true,
"lastVerifiedAt": "2025-11-04T10:00:00Z",
"reputationScore": 92,
"meetsRequiredLevel": true,
"registrantOrgName": "Bank Dev (Sample)"
}

Try a tenant-mismatch attempt:

grpcurl -plaintext -proto src/proto/sid.proto \
-d '{ "sender_id": "BANK-DEV", "type": "ALPHA", "tenant_id": "11111111-1111-1111-1111-111111111111" }' \
localhost:50091 ghasi.sms.sid.v1.SenderIdRegistryService/Verify

Expected: "status": "TENANT_MISMATCH".


8. Test the REST API

# Generate dev JWT
DEV_JWT=$(pnpm token:dev --tenant 00000000-0000-4000-8000-000000000001 --role tenant)

# Submit a sender-ID
curl -X POST -H "Authorization: Bearer $DEV_JWT" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d @test/fixtures/submit-retail.json \
http://localhost:3091/v1/sender-ids

# List own sender-IDs
curl -H "Authorization: Bearer $DEV_JWT" http://localhost:3091/v1/sender-ids

# Public search (no auth)
curl 'http://localhost:3091/v1/sender-ids/public/search?q=BANK-DEV'

# Admin: list reviewer queue
ADMIN_JWT=$(pnpm token:dev --role platform.sid.reviewer)
curl -H "Authorization: Bearer $ADMIN_JWT" \
'http://localhost:3091/v1/admin/sender-ids?state=KYC_REVIEW'

# Admin: approve a registration
curl -X POST -H "Authorization: Bearer $ADMIN_JWT" -H "Content-Type: application/json" \
-d '{"action":"APPROVE","reason":"All KYC verified"}' \
http://localhost:3091/v1/admin/sender-ids/sid_dev_pending/decision

9. Running Tests

# Unit tests (fast, no infra)
pnpm test

# Unit tests with coverage
pnpm test:cov

# Integration tests (requires Docker)
pnpm test:integration

# Contract tests
pnpm test:contract

# Watch mode
pnpm test:watch

# E2E tests
pnpm test:e2e

# Load test (requires ghz)
pnpm test:load:verify

10. Database Migrations

pnpm db:migrate:dev --name add_some_column # create migration
pnpm db:migrate # apply pending
pnpm db:reset # destructive: drop + reseed
pnpm db:partition:provision # provision next 3 monthly partitions

11. Helpful Dev Commands

# Trigger reputation cron manually
pnpm cli reputation:run

# Trigger regulator export manually
pnpm cli export:run --triggered-by ON_DEMAND

# Reset Redis caches
pnpm cli cache:clear

# Tail structured logs
pnpm logs:tail | jq

# Generate gRPC proto TypeScript types
pnpm proto:generate

# Add a restricted-pattern interactively
pnpm cli pattern:add --pattern '^FAKE.*$' --category OTHER_RESERVED --notes "Local test pattern"

# Inspect outbox lag
pnpm cli outbox:status

# Verify KYC blob hash (tamper check)
pnpm cli kyc:verify-hash --kyc-doc-id kyc_dev_001

12. Common Issues

IssueFix
Could not connect to PostgresEnsure docker compose --profile sid up -d is running; check DATABASE_URL
gRPC call hangsCheck GRPC_TLS_ENABLED=false in .env.local
DNS verification always failsEnsure mock-dns-server is running on port 8053 and contains the expected zone
OTP test cannot verifyCheck ./tmp/otp-log.jsonl for the issued OTP plaintext (mock channel-router writes here)
MinIO upload failsRun mc alias set local http://localhost:9000 ghasidev ghasidevsecret then mc mb local/ghasi-sid-kyc-local
HSM signing failsReinitialise SoftHSM token: pnpm hsm:init
Restricted-pattern not enforced after changeIn-process cache TTL 5 min; run pnpm cli cache:clear or wait
Tests fail with "relation does not exist"Run pnpm db:migrate

13. Useful URLs

  • MinIO console: http://localhost:9001 (ghasidev / ghasidevsecret)
  • NATS monitoring: http://localhost:8222
  • Postgres CLI: psql postgresql://ghasi:ghasi_dev@localhost:5432/ghasi_dev
  • Service metrics: http://localhost:3091/metrics
  • OpenAPI: http://localhost:3091/v1/sid/openapi.json