NSIF-X Developer Handbook · v1.0
MVP identity risk engine for SIM-swap & device-based fraud · FastAPI · Postgres · Redis
Ready for Dev Backend: FastAPI DB: PostgreSQL Cache: Redis
Section 0 NSIF-X Developer Manual · Purpose

This handbook explains how to develop, run, and extend the NSIF-X MVP – the National Secure Identity Fabric engine under the New Nigeria Security Grid.

After reading this, a developer should be able to:

  • Understand what NSIF-X does at a high level.
  • Spin up the full stack locally using Docker.
  • Explore the database and core identity models.
  • Use and extend the Risk Engine service.
  • Test and integrate the public APIs.
  • Hook up a demo dashboard for live presentations.
Audience: Backend / DevOps / Frontend Mode: MVP · Identity Risk Goal: Production-grade PoC
Section 1 Concept · What NSIF-X Does

NSIF-X (National Secure Identity Fabric – Experimental) is an identity risk scoring service for Nigeria’s digital ecosystem.

It combines:

  • MSISDN – phone numbers.
  • Device fingerprint – stable device hash from apps/web.
  • SIM-swap events – SIM change / replacement signals.
  • Event context – type (login/transfer/etc.), amount, channel, geo.

and outputs:

  • risk_score (0–100)
  • risk_level (low / medium / high / critical)
  • recommendation (allow / step_up_auth)
  • risk_factors (human-readable reasons)
Core MVP APIs
  • Risk Score API: /api/v1/risk-score
  • SIM Event API: /api/v1/sim/event
  • Device Register API: /api/v1/device/register
Business Use
  • Banks, telcos, fintechs call NSIF-X before high-risk actions.
  • Use recommendation to decide: allow, step-up, or block.
  • Anchor for New Nigeria Security Grid national rollout.
Key idea: NSIF-X does not replace existing KYC – it is an overlay identity risk engine that plugs into existing digital channels.
Section 2 Tech Stack & High-Level Architecture
Backend
  • Python 3.11+
  • FastAPI (REST APIs)
  • Uvicorn (ASGI server)
  • SQLAlchemy 2.x (ORM)
State
  • PostgreSQL – persistent store
  • Redis – cache & fast counters
Frontend (Demo Dashboard)
  • React / Next.js 13+ (App Router)
  • Tailwind CSS
Infra
  • Docker + docker-compose
  • Optional Adminer for DB UI
Architecture pattern: thin API layer → service layer (risk logic) → database & cache. Easy to split into microservices later (Risk Engine, SIM Events, Device Intelligence).
Section 3 Repository Structure

Recommended repo layout:

nsifx/
├── app/
│   ├── main.py
│   ├── api/
│   │   └── v1/
│   │       ├── __init__.py
│   │       ├── endpoints/
│   │       │   ├── risk.py
│   │       │   ├── device.py
│   │       │   └── sim.py
│   │       └── router.py
│   ├── core/
│   │   ├── config.py
│   │   └── security.py
│   ├── db/
│   │   ├── base.py
│   │   ├── models.py
│   │   ├── session.py
│   │   └── migrations/
│   ├── schemas/
│   │   ├── risk.py
│   │   ├── device.py
│   │   └── sim.py
│   ├── services/
│   │   ├── risk_engine.py
│   │   ├── device_service.py
│   │   └── sim_service.py
│   └── utils/
│       └── logging.py
├── infra/
│   ├── docker-compose.yml
│   ├── Dockerfile.backend
│   └── .env.example
├── tests/
│   └── test_risk_api.py
├── requirements.txt
└── README.md

Key ideas:

  • app/services holds business logic (risk engine, SIM logic).
  • app/api is thin: validate → call services → respond.
  • app/db centralises ORM models and DB session handling.
  • infra is where all Docker/deployment config lives.
Section 4 Local Setup · Docker-First
Prerequisites
  • Git
  • Docker & docker-compose
  • Python 3.11+ (only needed if running without Docker)
Clone & Configure
git clone <REPO_URL> nsifx
cd nsifx
cp infra/.env.example infra/.env
Start the Stack
cd infra
docker-compose up --build

Services exposed:

  • API: http://localhost:8000
  • Docs (Swagger): http://localhost:8000/api/v1/docs
  • Postgres: localhost:5432
  • Redis: localhost:6379
  • Adminer: http://localhost:8080
Tip: Keep all environment-specific values in infra/.env. Never hard-code secrets inside the codebase.
Section 5 Configuration · Settings & Env

Environment file: infra/.env

PROJECT_NAME="NSIF-X Risk Engine"
API_V1_STR="/api/v1"

POSTGRES_DSN=postgresql+psycopg2://nsifx_user:nsifx_password@db:5432/nsifx
REDIS_URL=redis://redis:6379/0

RISK_BASELINE=10
SIM_SWAP_RISK_BUMP=40
NEW_DEVICE_RISK_BUMP=25
MAX_RISK_SCORE=100
SIM_SWAP_COOLDOWN_HOURS=48

Settings class: app/core/config.py

from pydantic import BaseSettings, AnyUrl

class Settings(BaseSettings):
    PROJECT_NAME: str = "NSIF-X Risk Engine"
    API_V1_STR: str = "/api/v1"

    POSTGRES_DSN: AnyUrl
    REDIS_URL: str

    RISK_BASELINE: int = 10
    SIM_SWAP_RISK_BUMP: int = 40
    NEW_DEVICE_RISK_BUMP: int = 25
    MAX_RISK_SCORE: int = 100
    SIM_SWAP_COOLDOWN_HOURS: int = 48

    class Config:
        env_file = "infra/.env"

settings = Settings()
Section 6 Database Layer · Core Entities

Engine & Session: app/db/session.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from app.core.config import settings

engine = create_engine(settings.POSTGRES_DSN, pool_pre_ping=True, future=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine, future=True)
Base = declarative_base()

Key entities:

  • Device – registered devices (hash, trust_level, metadata).
  • PhoneIdentity – phone numbers & risk state.
  • PhoneDeviceLink – mapping phone ↔ device.
  • SimEvent – SIM swap/replacement history.
  • RiskEvent – audit trail of risk changes.

Example models (partial):

from sqlalchemy import Column, String, Integer, DateTime, JSON, ForeignKey, UniqueConstraint
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
import uuid
from app.db.session import Base

class Device(Base):
    __tablename__ = "devices"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    device_hash = Column(String(255), unique=True, nullable=False)
    first_seen_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
    last_seen_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
    trust_level = Column(String(20), nullable=False, default="unknown")
    metadata = Column(JSON)

class PhoneIdentity(Base):
    __tablename__ = "phone_identities"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    msisdn = Column(String(20), unique=True, nullable=False)
    created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
    last_seen_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
    risk_score = Column(Integer, nullable=False, default=0)
    risk_level = Column(String(20), nullable=False, default="low")
    cooldown_until = Column(DateTime(timezone=True), nullable=True)

class PhoneDeviceLink(Base):
    __tablename__ = "phone_device_links"
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    phone_id = Column(UUID(as_uuid=True), ForeignKey("phone_identities.id", ondelete="CASCADE"), nullable=False)
    device_id = Column(UUID(as_uuid=True), ForeignKey("devices.id", ondelete="CASCADE"), nullable=False)
    first_seen_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
    last_seen_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)

    __table_args__ = (
        UniqueConstraint("phone_id", "device_id", name="phone_device_unique"),
    )
Next step: add Alembic migrations to manage schema changes safely (app/db/migrations).
Section 7 Service Layer · Risk Engine Logic

File: app/services/risk_engine.py

Responsibilities:

  • Ensure PhoneIdentity exists for an msisdn.
  • Register new device hashes.
  • Link phones ↔ devices.
  • Apply SIM swap logic (risk bump + cooldown).
  • Compute final risk_score, risk_level & recommendation.

Example (simplified):

from datetime import datetime, timedelta
from typing import Tuple, List
from sqlalchemy.orm import Session
import redis

from app.core.config import settings
from app.db import models

r = redis.from_url(settings.REDIS_URL, decode_responses=True)

def _compute_risk_level(score: int) -> str:
    if score < 30:
        return "low"
    if score < 60:
        return "medium"
    if score < 85:
        return "high"
    return "critical"

def register_device(db: Session, device_hash: str, metadata: dict | None = None) -> models.Device:
    device = db.query(models.Device).filter_by(device_hash=device_hash).first()
    if not device:
        device = models.Device(device_hash=device_hash, metadata=metadata or {})
        db.add(device)
    device.last_seen_at = datetime.utcnow()
    db.commit()
    db.refresh(device)
    return device

def ensure_phone(db: Session, msisdn: str) -> models.PhoneIdentity:
    phone = db.query(models.PhoneIdentity).filter_by(msisdn=msisdn).first()
    if not phone:
        phone = models.PhoneIdentity(msisdn=msisdn, risk_score=settings.RISK_BASELINE)
        db.add(phone)
        db.commit()
        db.refresh(phone)
    phone.last_seen_at = datetime.utcnow()
    db.commit()
    db.refresh(phone)
    return phone

def apply_sim_swap_event(db: Session, msisdn: str) -> models.PhoneIdentity:
    phone = ensure_phone(db, msisdn)
    new_score = min(settings.MAX_RISK_SCORE, phone.risk_score + settings.SIM_SWAP_RISK_BUMP)
    phone.risk_score = new_score
    phone.risk_level = _compute_risk_level(new_score)
    phone.cooldown_until = datetime.utcnow() + timedelta(hours=settings.SIM_SWAP_COOLDOWN_HOURS)
    db.commit()
    return phone

def calculate_risk_for_event(
    db: Session, msisdn: str, device_hash: str, event_type: str, amount: float | None = None
) -> Tuple[int, str, str, List[str]]:
    factors: List[str] = []
    phone = ensure_phone(db, msisdn)
    device = register_device(db, device_hash, metadata=None)

    factors.append("baseline_identity_risk")
    score = phone.risk_score

    if phone.cooldown_until and phone.cooldown_until > datetime.utcnow():
        factors.append("recent_sim_swap_cooldown")
        score = min(settings.MAX_RISK_SCORE, score + 20)

    if event_type == "transfer" and amount and amount > 100000:
        factors.append("high_value_transfer")
        score = min(settings.MAX_RISK_SCORE, score + 10)

    phone.risk_score = score
    phone.risk_level = _compute_risk_level(score)
    db.commit()
    db.refresh(phone)

    if phone.risk_level in ("high", "critical"):
        recommendation = "step_up_auth"
    else:
        recommendation = "allow"

    return phone.risk_score, phone.risk_level, recommendation, factors
Extend later: device velocity, cross-institution signals, NIN/BVN linkage, behavioural analytics, and network intelligence.
Section 8 API Layer · FastAPI Endpoints

Main app: app/main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.api.v1.router import api_router

app = FastAPI(
    title=settings.PROJECT_NAME,
    version="0.1.0",
    openapi_url=f"{settings.API_V1_STR}/openapi.json",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(api_router, prefix=settings.API_V1_STR)

@app.get("/")
def healthcheck():
    return {"status": "ok", "service": settings.PROJECT_NAME}

Router: app/api/v1/router.py

from fastapi import APIRouter
from .endpoints import risk, device, sim

api_router = APIRouter()
api_router.include_router(risk.router, tags=["risk"])
api_router.include_router(device.router, tags=["device"])
api_router.include_router(sim.router, tags=["sim"])
Risk Score Endpoint
@router.post("/risk-score", response_model=RiskScoreResponse)
def get_risk_score(payload: RiskScoreRequest, db: Session = Depends(get_db)):
    score, level, recommendation, factors = risk_engine.calculate_risk_for_event(
        db=db,
        msisdn=payload.msisdn,
        device_hash=payload.device_hash,
        event_type=payload.event_type,
        amount=payload.amount,
    )
    return RiskScoreResponse(
        risk_score=score,
        risk_level=level,
        recommendation=recommendation,
        risk_factors=factors,
    )
SIM Event & Device Register

Similar FastAPI endpoints in:

  • app/api/v1/endpoints/sim.py
  • app/api/v1/endpoints/device.py
Important: keep API contracts stable; treat them as the “public face” that banks, telcos and government will integrate against.
Section 9 Pydantic Schemas · Input & Output

Risk schemas: app/schemas/risk.py

from pydantic import BaseModel, Field
from typing import Optional, List

class RiskScoreRequest(BaseModel):
    msisdn: str = Field(..., example="+2348012345678")
    device_hash: str = Field(..., example="abcdef1234567890")
    event_type: str = Field(..., example="login")
    amount: Optional[float] = Field(None, example=200000.0)
    channel: Optional[str] = Field(None, example="mobile-app")
    geo: Optional[str] = Field(None, example="Lagos")

class RiskScoreResponse(BaseModel):
    risk_score: int
    risk_level: str
    recommendation: str
    risk_factors: List[str]

Device schemas: app/schemas/device.py

from pydantic import BaseModel, Field
from typing import Optional, Dict

class DeviceRegisterRequest(BaseModel):
    device_hash: str = Field(..., example="abcdef1234567890")
    metadata: Optional[Dict[str, str]] = None

class DeviceRegisterResponse(BaseModel):
    device_id: str
    trust_level: str

SIM schemas: app/schemas/sim.py

from pydantic import BaseModel, Field
from typing import Optional, Dict

class SimEventRequest(BaseModel):
    msisdn: str = Field(..., example="+2348012345678")
    event_type: str = Field(..., example="SIM_SWAP")
    channel: Optional[str] = Field(None, example="retail_agent")
    metadata: Optional[Dict[str, str]] = None
Section 10 Manual API Testing · cURL Quickstart

Once the stack is running, you can test APIs directly.

Healthcheck
curl http://localhost:8000/
Register Device
curl -X POST http://localhost:8000/api/v1/device/register \
  -H "Content-Type: application/json" \
  -d '{
    "device_hash": "test-device-abc123",
    "metadata": {"platform": "android"}
  }'
SIM Swap Event
curl -X POST http://localhost:8000/api/v1/sim/event \
  -H "Content-Type: application/json" \
  -d '{
    "msisdn": "+2348012345678",
    "event_type": "SIM_SWAP",
    "channel": "retail_agent"
  }'
Risk Score
curl -X POST http://localhost:8000/api/v1/risk-score \
  -H "Content-Type: application/json" \
  -d '{
    "msisdn": "+2348012345678",
    "device_hash": "test-device-abc123",
    "event_type": "login",
    "amount": 200000,
    "channel": "mobile-app",
    "geo": "Lagos"
  }'
Tip: use Postman or Bruno collections later for nicer team testing; keep cURL examples in README for quick validation.
Section 11 Demo Dashboard · React / Next.js

Goal: Let stakeholders see risk scoring in real time.

Frontend stack:

  • Next.js 13+ (App Router) or React SPA.
  • Tailwind CSS for quick styling.

Environment variables:

  • NEXT_PUBLIC_API_BASE → e.g. http://localhost:8000/api/v1
  • NEXT_PUBLIC_API_KEY → for auth (later).

Core UI sections:

  • Identity panel – MSISDN + device hash input.
  • Buttons – “Register Device”, “Simulate SIM Swap”, “Get Risk Score”.
  • Result panel – big score, level badge, recommendation, risk factors list.
Use the dashboard to show:
  • Risk before SIM swap.
  • Trigger SIM swap → bump in risk_score and risk_level.
  • How recommendation changes to step_up_auth.
Section 12 Security & Hardening · Next Steps

For demos you can keep it light; for production, you must add:

  • API authentication (API keys or JWT/OAuth2) in security.py.
  • Locked-down CORS (specific origins only).
  • Rate limiting for public endpoints (Redis-backed).
  • Request/response logging with correlation IDs.
  • Audit logging for risk changes and SIM events.
  • Alembic migrations for DB schema evolution.
Regulatory angle: NSIF-X should ultimately align with NDPA/NDPR, CBN, NCC, NITDA, and ONSA guidelines. Build with auditability and data minimisation from day one.
Section 13 Development Workflow · Branching & Testing

Recommended workflow:

  • Branches:
    • main – stable.
    • dev – integration.
    • feature branches – e.g. feature/velocity-metrics.
  • PR rules:
    • All tests pass (pytest).
    • Public APIs documented if changed.
    • Security impact briefly assessed.
  • Tests:
    • Unit tests for risk_engine.
    • Integration tests for endpoints (FastAPI TestClient).
Section 14 Roadmap for Developers

After MVP stabilises:

  • Velocity & behaviour:
    • Count logins / transfers in sliding windows.
    • Detect unusual device changes per MSISDN.
  • NIN/BVN integration:
    • Optional linking to national identifiers for deeper risk context.
  • Admin portal:
    • RBAC, institution-level views, risk configuration per client.
  • Analytics & reporting:
    • Fraud heatmaps, SIM-swap clusters, high-risk device insights.
  • Multi-tenant design:
    • Institution separation for banks/telcos.
    • Shared intelligence, private data slices.