The Trade Compliance Platform is a SaaS Trade Intelligence system serving Importers, Exporters, and Custom Brokers. It automates document readiness assessment, regulatory compliance verification, sanctions screening, HS code classification, and tariff rate lookup for international trade transactions.
The platform follows a Zero-Trust Compliance philosophy: every compliance claim is traceable to a source document or regulation, with a tamper-evident audit trail for every decision.
- Phase 1 — Document Readiness: Shipment creation, supplier invitation via magic links, AI-powered document scanning/extraction/scoring, completeness checking, and document versioning.
- Phase 2 — Document Compliance Check: Regulatory compliance verification, sanctions screening (OFAC, EU, UN), HS code classification, tariff rate lookup, and trade route intelligence.
India, Canada, USA — with a country-agnostic data model designed for global expansion.
| Decision | Choice | Rationale |
|---|---|---|
| Frontend | Next.js 16 (App Router) | Best-in-class React framework, SSR/SSG, App Router for layouts |
| API Gateway | NestJS | TypeScript, decorators for guards/interceptors, modular architecture |
| AI/ML Services | Python FastAPI | Python ML ecosystem, async support, LangGraph compatibility |
| Inter-service Comms | BullMQ (Redis) + Direct API | Async jobs for heavy processing, direct calls for simple lookups |
| Agent Orchestration | LangGraph (Hierarchical Supervisor) | Stateful agent workflows, tool calling, human-in-the-loop |
| Primary DB | PostgreSQL + RLS | ACID compliance, Row-Level Security for multi-tenancy |
| Knowledge Graph | Neo4j | Native graph traversal for regulatory relationships |
| Vector Store | pgvector | Collocated with PostgreSQL, no extra infra for MVP |
| Cache/Queue | Redis | BullMQ backing, rate limiting, tariff cache |
| Auth | Clerk | SOC 2 compliant, org management, RBAC, B2B multi-tenant |
| Deployment | Serverful (ECS Fargate) | Persistent connections for agents, GPU for doc processing |
| IaC | Terraform | AWS infrastructure provisioning |
| Local Dev | Docker Compose + Ollama | Full stack local with local LLM inference |
graph TB
subgraph "Client Layer"
WEB[Next.js 16 Frontend<br/>App Router + RSC]
MAGIC[Magic Link Portal<br/>Exporter Upload UI]
end
subgraph "API Layer"
GW[NestJS API Gateway<br/>Auth · RBAC · Rate Limiting<br/>Input Validation · CORS]
end
subgraph "Processing Layer"
AI[Python FastAPI<br/>AI/ML Service]
BULL[BullMQ Workers<br/>Job Processing]
LG[LangGraph Supervisor<br/>Agent Orchestration]
end
subgraph "Agent Layer"
SUP[Supervisor Agent]
DP[Document Parser Agent]
CC[Compliance Checker Agent]
SS[Sanctions Screener Agent]
HSC[HS Classifier Agent]
TL[Tariff Lookup Agent]
end
subgraph "Data Layer"
PG[(PostgreSQL + pgvector<br/>RLS Multi-Tenancy)]
NEO[(Neo4j<br/>Knowledge Graph)]
REDIS[(Redis<br/>Cache + Queues)]
S3[(AWS S3<br/>Document Storage)]
end
subgraph "External Services"
CLERK[Clerk Auth]
SES[AWS SES<br/>Email]
BEDROCK[AWS Bedrock<br/>LLM Inference]
TEXTRACT[AWS Textract<br/>OCR]
end
WEB --> GW
MAGIC --> GW
GW --> AI
GW --> PG
GW --> REDIS
GW --> BULL
BULL --> AI
AI --> LG
LG --> SUP
SUP --> DP
SUP --> CC
SUP --> SS
SUP --> HSC
SUP --> TL
AI --> PG
AI --> NEO
AI --> REDIS
DP --> TEXTRACT
DP --> BEDROCK
CC --> NEO
SS --> NEO
HSC --> NEO
TL --> NEO
GW --> CLERK
GW --> S3
AI --> S3
GW --> SES
graph TB
subgraph "AWS Cloud"
subgraph "Public Subnet"
ALB[Application Load Balancer<br/>TLS Termination]
CF[CloudFront CDN<br/>Static Assets]
end
subgraph "Private Subnet - Application"
ECS_WEB[ECS Fargate<br/>Next.js Frontend]
ECS_GW[ECS Fargate<br/>NestJS API Gateway]
ECS_AI[ECS Fargate<br/>Python AI Service<br/>GPU-enabled]
ECS_WORKER[ECS Fargate<br/>BullMQ Workers]
end
subgraph "Private Subnet - Data"
RDS[(RDS PostgreSQL<br/>Multi-AZ + pgvector)]
NEO4J[(Neo4j Aura<br/>or EC2 Neo4j)]
ELASTICACHE[(ElastiCache Redis<br/>Cluster Mode)]
S3_DOCS[(S3 Bucket<br/>Document Storage<br/>SSE-KMS)]
end
subgraph "Security & Monitoring"
KMS[AWS KMS<br/>Tenant-Specific Keys]
WAF[AWS WAF<br/>Web Application Firewall]
CW[CloudWatch<br/>Logs + Metrics + Alarms]
CT[CloudTrail<br/>API Audit]
GD[GuardDuty<br/>Threat Detection]
end
subgraph "AI/ML Services"
BEDROCK_SVC[AWS Bedrock<br/>Claude Sonnet 4.5 · Nova Pro<br/>Nova Lite · Nova Micro<br/>Titan Embeddings]
TEXTRACT_SVC[AWS Textract<br/>OCR Preprocessing]
end
end
subgraph "External"
CLERK_EXT[Clerk Auth Service]
OFAC[OFAC SDN Feed]
EU_SANC[EU Sanctions Feed]
UN_SANC[UN Sanctions Feed]
end
CF --> ALB
ALB --> ECS_WEB
ALB --> ECS_GW
ECS_GW --> ECS_AI
ECS_GW --> ELASTICACHE
ECS_GW --> RDS
ECS_WORKER --> ELASTICACHE
ECS_WORKER --> ECS_AI
ECS_AI --> RDS
ECS_AI --> NEO4J
ECS_AI --> BEDROCK_SVC
ECS_AI --> TEXTRACT_SVC
ECS_AI --> S3_DOCS
ECS_GW --> CLERK_EXT
ECS_GW --> S3_DOCS
WAF --> ALB
KMS --> S3_DOCS
KMS --> RDS
graph TB
subgraph "Docker Compose"
NEXT[Next.js 16<br/>Port 3000]
NEST[NestJS Gateway<br/>Port 3001]
FASTAPI[FastAPI AI Service<br/>Port 8000]
WORKER[BullMQ Worker<br/>Background]
PG_LOCAL[(PostgreSQL 16<br/>Port 5432)]
NEO_LOCAL[(Neo4j<br/>Port 7474/7687)]
REDIS_LOCAL[(Redis<br/>Port 6379)]
OLLAMA[Ollama<br/>Port 11434<br/>Llama 3.1 8B<br/>Mistral 7B<br/>Gemma 3 4B<br/>nomic-embed-text]
MINIO[MinIO<br/>S3-Compatible<br/>Port 9000]
end
NEXT --> NEST
NEST --> FASTAPI
NEST --> PG_LOCAL
NEST --> REDIS_LOCAL
NEST --> MINIO
WORKER --> REDIS_LOCAL
WORKER --> FASTAPI
FASTAPI --> PG_LOCAL
FASTAPI --> NEO_LOCAL
FASTAPI --> OLLAMA
FASTAPI --> MINIO
sequenceDiagram
participant C as Client (Next.js)
participant CK as Clerk
participant GW as NestJS Gateway
participant R as Redis
participant Q as BullMQ Queue
participant AI as FastAPI AI Service
participant LG as LangGraph Supervisor
participant A as Specialized Agent
participant PG as PostgreSQL
participant NG as Neo4j
participant S3 as S3/MinIO
C->>CK: Authenticate
CK-->>C: JWT Token
C->>GW: API Request + JWT
GW->>CK: Verify JWT
CK-->>GW: User + Org Claims
GW->>R: Check Rate Limit
R-->>GW: OK / 429
GW->>GW: RBAC Check (role + tenant)
GW->>GW: Input Validation + Sanitization
alt Synchronous (Simple Lookup)
GW->>PG: Query with tenant_id RLS
PG-->>GW: Result
GW-->>C: JSON Response
end
alt Asynchronous (AI Processing)
GW->>Q: Enqueue Job
GW-->>C: 202 Accepted + Job ID
Q->>AI: Dequeue Job
AI->>LG: Dispatch to Supervisor
LG->>A: Delegate to Agent
A->>NG: Query Knowledge Graph
A->>S3: Fetch Document
A-->>LG: Agent Result
LG-->>AI: Aggregated Result
AI->>PG: Store Results
AI->>R: Publish Notification
C->>GW: Poll Job Status
GW->>PG: Get Job Result
GW-->>C: Job Result
end
Responsibility: Server-side rendered UI, client-side interactivity, Clerk auth integration.
Key Routes (App Router):
| Route | Description | Auth Required |
|---|---|---|
/ |
Landing page | No |
/dashboard |
Readiness Dashboard | Yes |
/shipments |
Shipment list | Yes |
/shipments/[id] |
Shipment detail + documents | Yes |
/shipments/new |
Create shipment wizard | Yes |
/compliance/[shipmentId] |
Compliance report | Yes |
/tariffs/[shipmentId] |
Tariff lookup results | Yes |
/suppliers |
Supplier Network directory | Yes |
/admin |
Platform admin panel | Yes (Admin) |
/admin/demo |
Demo management | Yes (Admin) |
/portal/[token] |
Magic Link exporter portal | Magic Link Token |
/portal/[token]/signup |
Exporter sign-up | Magic Link Token |
Security Headers (Next.js Middleware):
Content-Security-Policy: default-src 'self'; script-src 'self' https://clerk.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://api.clerk.com https://api.tradecomplianceplatform.com;
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
Frontend Security:
- DOMPurify for all user-generated content rendering
- CSRF tokens for all state-changing form submissions (SameSite=Strict cookies + double-submit pattern)
- No
dangerouslySetInnerHTMLwithout sanitization - All API calls via server actions or API routes (no direct client-to-backend)
Responsibility: Authentication, authorization, rate limiting, input validation, request routing, audit logging.
Module Structure:
src/
├── main.ts
├── app.module.ts
├── common/
│ ├── guards/
│ │ ├── clerk-auth.guard.ts # JWT verification via Clerk
│ │ ├── rbac.guard.ts # Role-based access control
│ │ ├── tenant.guard.ts # Tenant isolation enforcement
│ │ └── magic-link.guard.ts # Magic link token validation
│ ├── interceptors/
│ │ ├── audit-log.interceptor.ts # Automatic audit trail logging
│ │ ├── tenant-context.interceptor.ts # Inject tenant_id into request
│ │ └── response-transform.interceptor.ts
│ ├── decorators/
│ │ ├── roles.decorator.ts # @Roles('Importer', 'Custom_Broker')
│ │ ├── permissions.decorator.ts # @Permissions('shipment:write')
│ │ └── current-user.decorator.ts # @CurrentUser() user extraction
│ ├── filters/
│ │ └── global-exception.filter.ts
│ ├── pipes/
│ │ └── validation.pipe.ts # class-validator integration
│ └── middleware/
│ ├── cors.middleware.ts # Strict origin whitelist
│ ├── helmet.middleware.ts # Security headers
│ ├── csrf.middleware.ts # CSRF protection
│ └── rate-limit.middleware.ts # Per-user/per-IP rate limiting
├── modules/
│ ├── auth/
│ │ ├── auth.module.ts
│ │ ├── auth.controller.ts
│ │ ├── auth.service.ts
│ │ └── strategies/
│ │ ├── clerk.strategy.ts
│ │ └── magic-link.strategy.ts
│ ├── shipments/
│ │ ├── shipments.module.ts
│ │ ├── shipments.controller.ts
│ │ ├── shipments.service.ts
│ │ └── dto/
│ │ ├── create-shipment.dto.ts
│ │ └── update-shipment.dto.ts
│ ├── documents/
│ │ ├── documents.module.ts
│ │ ├── documents.controller.ts
│ │ ├── documents.service.ts
│ │ └── dto/
│ │ └── upload-document.dto.ts
│ ├── compliance/
│ │ ├── compliance.module.ts
│ │ ├── compliance.controller.ts
│ │ └── compliance.service.ts
│ ├── tariffs/
│ │ ├── tariffs.module.ts
│ │ ├── tariffs.controller.ts
│ │ └── tariffs.service.ts
│ ├── magic-links/
│ │ ├── magic-links.module.ts
│ │ ├── magic-links.controller.ts
│ │ └── magic-links.service.ts
│ ├── notifications/
│ │ ├── notifications.module.ts
│ │ └── notifications.service.ts
│ ├── jobs/
│ │ ├── jobs.module.ts
│ │ ├── jobs.controller.ts
│ │ └── jobs.service.ts
│ ├── audit/
│ │ ├── audit.module.ts
│ │ └── audit.service.ts
│ ├── suppliers/
│ │ ├── suppliers.module.ts
│ │ ├── suppliers.controller.ts
│ │ └── suppliers.service.ts
│ ├── admin/
│ ├── admin.module.ts
│ ├── admin.controller.ts
│ ├── demo.controller.ts
│ └── demo.service.ts
│ └── billing/
│ ├── billing.module.ts
│ ├── billing.controller.ts
│ ├── billing.service.ts
│ ├── stripe-webhook.controller.ts
│ ├── plan-limits.middleware.ts
│ └── dto/
│ ├── create-subscription.dto.ts
│ └── update-subscription.dto.ts
└── config/
├── database.config.ts
├── redis.config.ts
├── clerk.config.ts
├── cors.config.ts
└── rate-limit.config.ts
API Endpoints:
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/webhook |
Clerk Webhook | Clerk user sync webhook |
| GET | /api/v1/shipments |
JWT + Tenant | List shipments |
| POST | /api/v1/shipments |
JWT + Tenant | Create shipment |
| GET | /api/v1/shipments/:id |
JWT + Tenant | Get shipment detail |
| PUT | /api/v1/shipments/:id |
JWT + Tenant | Update shipment |
| POST | /api/v1/shipments/:id/documents |
JWT + Tenant | Upload document |
| GET | /api/v1/shipments/:id/documents |
JWT + Tenant | List documents |
| GET | /api/v1/shipments/:id/documents/:docId/versions |
JWT + Tenant | Document version history |
| POST | /api/v1/shipments/:id/compliance/check |
JWT + Tenant | Initiate compliance check |
| GET | /api/v1/shipments/:id/compliance/report |
JWT + Tenant | Get compliance report |
| GET | /api/v1/shipments/:id/compliance/report/pdf |
JWT + Tenant | Download compliance PDF |
| GET | /api/v1/shipments/:id/tariffs |
JWT + Tenant | Tariff lookup |
| POST | /api/v1/shipments/:id/magic-links |
JWT + Tenant | Generate magic link |
| GET | /api/v1/portal/:token |
Magic Link | Get portal data |
| POST | /api/v1/portal/:token/documents |
Magic Link | Upload via portal |
| POST | /api/v1/portal/:token/signup |
Magic Link | Exporter sign-up |
| GET | /api/v1/jobs/:jobId |
JWT + Tenant | Job status |
| GET | /api/v1/suppliers/search |
JWT + Tenant | Search supplier network |
| GET | /api/v1/trade-corridors/:corridor/requirements |
JWT + Tenant | Trade route requirements |
| POST | /api/v1/sanctions/:shipmentId/review |
JWT + Tenant | Review sanctions hit |
| GET | /api/v1/dashboard/readiness |
JWT + Tenant | Dashboard data |
| POST | /api/v1/admin/demo/reset |
JWT + Admin | Reset demo tenant |
| GET | /api/v1/health |
None | Health check |
| POST | /api/v1/billing/webhook |
Stripe Signature | Stripe webhook endpoint |
| GET | /api/v1/billing/subscription |
JWT + Tenant | Get current subscription |
| POST | /api/v1/billing/subscription |
JWT + Tenant | Create/update subscription |
| GET | /api/v1/billing/usage |
JWT + Tenant | Get usage records |
| POST | /api/v1/billing/portal-session |
JWT + Tenant | Create Stripe billing portal session |
CORS Configuration:
// cors.config.ts
const ALLOWED_ORIGINS = {
production: ['https://app.tradecomplianceplatform.com'],
staging: ['https://staging.tradecomplianceplatform.com'],
development: ['http://localhost:3000'],
};
// No wildcards in production. Origin validated per-request.Rate Limiting Configuration:
// rate-limit.config.ts
{
authenticated: { ttl: 60_000, limit: 100 }, // 100 req/min per user
unauthenticated: { ttl: 60_000, limit: 20 }, // 20 req/min per IP
magicLink: { ttl: 86_400_000, limit: 10 }, // 10 links/shipment/day
authAttempts: { ttl: 900_000, limit: 5 }, // 5 attempts/15 min
}Responsibility: Agent orchestration, document processing, compliance engine, ML inference.
Project Structure:
ai_service/
├── main.py
├── config/
│ ├── settings.py # Environment-based config
│ ├── models.py # LLM/SLM model registry
│ └── ollama.py # Local Ollama config
├── api/
│ ├── routes/
│ │ ├── documents.py # Document processing endpoints
│ │ ├── compliance.py # Compliance check endpoints
│ │ ├── sanctions.py # Sanctions screening endpoints
│ │ ├── classification.py # HS code classification endpoints
│ │ └── health.py
│ └── dependencies.py # FastAPI dependencies
├── agents/
│ ├── supervisor.py # LangGraph Supervisor Agent
│ ├── document_parser.py # Document Parser Agent
│ ├── compliance_checker.py # Compliance Checker Agent
│ ├── sanctions_screener.py # Sanctions Screener Agent
│ ├── hs_classifier.py # HS Classifier Agent
│ ├── tariff_lookup.py # Tariff Lookup Agent
│ ├── tools/
│ │ ├── ocr_tool.py # Textract / Ollama Vision wrapper
│ │ ├── neo4j_tool.py # Knowledge Graph query tool
│ │ ├── vector_search.py # pgvector RAG search tool
│ │ ├── sanctions_db.py # Sanctions list lookup tool
│ │ └── tariff_db.py # Tariff rate lookup tool
│ └── graphs/
│ ├── document_graph.py # Document processing workflow
│ ├── compliance_graph.py # Compliance check workflow
│ └── screening_graph.py # Sanctions screening workflow
├── models/
│ ├── router.py # Model Router (complexity-based)
│ ├── ollama_client.py # Ollama local inference
│ └── bedrock_client.py # AWS Bedrock inference
├── workers/
│ ├── bullmq_consumer.py # BullMQ job consumer
│ ├── document_worker.py # Document processing worker
│ ├── compliance_worker.py # Compliance check worker
│ └── sanctions_worker.py # Sanctions screening worker
├── ingestion/
│ ├── scheduler.py # Data refresh scheduler
│ ├── adapters/
│ │ ├── base.py # Base adapter interface
│ │ ├── ofac_adapter.py # OFAC SDN ingestion
│ │ ├── eu_sanctions.py # EU sanctions ingestion
│ │ ├── un_sanctions.py # UN sanctions ingestion
│ │ ├── usitc_hts.py # US HTS data ingestion
│ │ ├── canada_tariff.py # Canadian tariff ingestion
│ │ └── india_dgft.py # India DGFT ingestion
│ └── validators/
│ └── schema_validator.py # Data schema validation
├── knowledge_graph/
│ ├── client.py # Neo4j driver wrapper
│ ├── queries.py # Cypher query templates
│ └── versioning.py # KG version management
├── vector_store/
│ ├── client.py # pgvector client
│ └── embeddings.py # Embedding generation
├── services/
│ ├── scoring.py # Document scoring logic
│ ├── checklist.py # Document checklist generation
│ ├── fuzzy_match.py # Sanctions fuzzy matching
│ └── po_parser.py # PO parsing and extraction
└── db/
├── session.py # SQLAlchemy async session
└── models.py # SQLAlchemy ORM models
LLM/SLM Tiered Model Strategy:
The platform uses a tiered model approach: SLMs (small language models) for fast/cheap tasks, LLMs only for accuracy-critical compliance work. This optimizes cost while maintaining compliance accuracy.
Production Model Tiers (AWS Bedrock):
| Tier | Model | Use Cases | Cost (per M tokens) | Context | Notes |
|---|---|---|---|---|---|
| Tier 1 — Heavy Reasoning | Claude Sonnet 4.5 | Compliance analysis, cross-reference verification, complex regulatory interpretation | $3 / $15 (in/out) | 200K | SWE-bench 77.2%, best reasoning |
| Tier 2 — HS Classification & Doc Understanding | Amazon Nova Pro | HS code classification, document understanding, trade corridor analysis | ~75% cheaper than comparable | 300K | Multimodal, excellent for document analysis |
| Tier 3 — Fast Extraction & Simple Tasks | Amazon Nova Lite | Field extraction from documents, simple data parsing, template matching | Very low | 300K | Multimodal (text+images), fast |
| Tier 4 — Text-only Quick Tasks | Amazon Nova Micro | Agent routing decisions, classification, summarization, intent detection | Lowest | 128K | Text-only, 200+ tokens/sec |
| Tier 5 — Vision/OCR | Amazon Nova Lite + AWS Textract | OCR preprocessing, image-based document scanning | Low + Textract pricing | — | Textract for layout, Nova Lite for understanding |
| Tier 6 — Embeddings | Amazon Titan Embed Text v2 | RAG embeddings, semantic search | Very low | — | 768-dim vectors |
Local Dev Model Tiers (Ollama):
| Tier | Model | Use Cases | RAM Requirement | Notes |
|---|---|---|---|---|
| Heavy Reasoning | Llama 3.1 8B | Compliance analysis, complex reasoning | ~8GB (fits 32GB + RTX 3060) | Quantized for local dev |
| Fast Extraction | Mistral 7B | Field extraction, simple parsing | ~6GB | Fast inference |
| Vision/OCR | Gemma 3 4B | Document OCR, image understanding | ~4GB | Multimodal, lightweight, runs well on RTX 3060 |
| Embeddings | nomic-embed-text | RAG embeddings | ~1GB | 768-dim vectors |
Model Router:
# models/router.py
from enum import Enum
from typing import Optional
class TaskType(Enum):
COMPLIANCE_ANALYSIS = "compliance_analysis" # Tier 1: Heavy reasoning
CROSS_REFERENCE = "cross_reference" # Tier 1: Heavy reasoning
HS_CLASSIFICATION = "hs_classification" # Tier 2: Document understanding
DOCUMENT_UNDERSTANDING = "document_understanding" # Tier 2: Document understanding
FIELD_EXTRACTION = "field_extraction" # Tier 3: Fast extraction
TEMPLATE_MATCHING = "template_matching" # Tier 3: Fast extraction
AGENT_ROUTING = "agent_routing" # Tier 4: Quick text tasks
INTENT_CLASSIFICATION = "intent_classification" # Tier 4: Quick text tasks
SUMMARIZATION = "summarization" # Tier 4: Quick text tasks
VISION_OCR = "vision_ocr" # Tier 5: Vision/OCR
EMBEDDINGS = "embeddings" # Tier 6: Embeddings
MODEL_ROUTING = {
"local": {
TaskType.COMPLIANCE_ANALYSIS: "llama3.1:8b",
TaskType.CROSS_REFERENCE: "llama3.1:8b",
TaskType.HS_CLASSIFICATION: "llama3.1:8b",
TaskType.DOCUMENT_UNDERSTANDING: "llama3.1:8b",
TaskType.FIELD_EXTRACTION: "mistral:7b",
TaskType.TEMPLATE_MATCHING: "mistral:7b",
TaskType.AGENT_ROUTING: "mistral:7b",
TaskType.INTENT_CLASSIFICATION: "mistral:7b",
TaskType.SUMMARIZATION: "mistral:7b",
TaskType.VISION_OCR: "gemma3:4b",
TaskType.EMBEDDINGS: "nomic-embed-text",
},
"production": {
# Tier 1 — Heavy Reasoning (Claude Sonnet 4.5)
TaskType.COMPLIANCE_ANALYSIS: "anthropic.claude-sonnet-4-5-20250514-v1:0",
TaskType.CROSS_REFERENCE: "anthropic.claude-sonnet-4-5-20250514-v1:0",
# Tier 2 — HS Classification & Document Understanding (Nova Pro)
TaskType.HS_CLASSIFICATION: "amazon.nova-pro-v1:0",
TaskType.DOCUMENT_UNDERSTANDING: "amazon.nova-pro-v1:0",
# Tier 3 — Fast Extraction (Nova Lite)
TaskType.FIELD_EXTRACTION: "amazon.nova-lite-v1:0",
TaskType.TEMPLATE_MATCHING: "amazon.nova-lite-v1:0",
# Tier 4 — Quick Text Tasks (Nova Micro)
TaskType.AGENT_ROUTING: "amazon.nova-micro-v1:0",
TaskType.INTENT_CLASSIFICATION: "amazon.nova-micro-v1:0",
TaskType.SUMMARIZATION: "amazon.nova-micro-v1:0",
# Tier 5 — Vision/OCR (Nova Lite + Textract)
TaskType.VISION_OCR: "amazon.nova-lite-v1:0", # + aws.textract for preprocessing
# Tier 6 — Embeddings (Titan)
TaskType.EMBEDDINGS: "amazon.titan-embed-text-v2:0",
}
}
class ModelRouter:
def __init__(self, environment: str = "production"):
self.environment = environment
self.routing = MODEL_ROUTING[environment]
def route(self, task_type: TaskType) -> str:
"""Route to the appropriate model based on task type.
Principle: Use SLMs (Nova Micro, Nova Lite, Gemma 3) for simple/fast tasks,
LLMs (Claude 4.5, Nova Pro) only for accuracy-critical compliance work.
"""
return self.routing[task_type]
def needs_textract_preprocessing(self, task_type: TaskType) -> bool:
"""Whether this task needs AWS Textract OCR before LLM processing."""
return task_type == TaskType.VISION_OCR and self.environment == "production"Supervisor Pattern:
graph TB
subgraph "LangGraph Supervisor"
SUP[Supervisor Agent<br/>Task Router + State Manager]
end
subgraph "Specialized Agents"
DP[Document Parser<br/>OCR + NLP Extraction<br/>Field Confidence Scoring]
CC[Compliance Checker<br/>Regulation Verification<br/>Cross-Reference Validation]
SS[Sanctions Screener<br/>Fuzzy Name Matching<br/>OFAC/EU/UN Lists]
HSC[HS Classifier<br/>Product Classification<br/>KG + RAG Lookup]
TL[Tariff Lookup<br/>Rate Retrieval<br/>Trade Agreement Check]
end
subgraph "Tools"
OCR[OCR Tool<br/>Textract / Vision LLM]
KG[Knowledge Graph Tool<br/>Neo4j Cypher Queries]
VS[Vector Search Tool<br/>pgvector RAG]
SDB[Sanctions DB Tool<br/>Fuzzy Match Engine]
TDB[Tariff DB Tool<br/>Rate Cache + KG]
end
SUP --> DP
SUP --> CC
SUP --> SS
SUP --> HSC
SUP --> TL
DP --> OCR
DP --> VS
CC --> KG
CC --> VS
SS --> SDB
HSC --> KG
HSC --> VS
TL --> TDB
TL --> KG
Agent Workflow — Document Processing:
sequenceDiagram
participant W as BullMQ Worker
participant S as Supervisor Agent
participant DP as Document Parser
participant OCR as OCR Tool
participant LLM as LLM (Claude/Llama)
participant PG as PostgreSQL
participant R as Redis
W->>S: Process Document Job
S->>S: Determine document type
S->>DP: Dispatch to Document Parser
DP->>OCR: Extract raw text + layout
OCR-->>DP: Raw text + bounding boxes
DP->>LLM: Extract structured fields
LLM-->>DP: Structured data + confidence
DP->>DP: Validate extracted fields
DP->>DP: Compute Document Score
DP-->>S: Extraction Result + Score
S->>PG: Store extracted data + score
S->>R: Publish score update event
S-->>W: Job Complete
Agent Workflow — Compliance Check:
sequenceDiagram
participant W as BullMQ Worker
participant S as Supervisor Agent
participant CC as Compliance Checker
participant SS as Sanctions Screener
participant KG as Neo4j Knowledge Graph
participant LLM as LLM
participant PG as PostgreSQL
W->>S: Compliance Check Job
S->>SS: Screen all parties
SS->>KG: Query sanctions lists
KG-->>SS: Matching entities
SS->>SS: Fuzzy match (threshold 85%)
SS-->>S: Screening results
alt Sanctions Hit Found
S->>PG: Store hit, block shipment
S-->>W: Job Complete (Blocked)
end
S->>CC: Check document compliance
CC->>KG: Get corridor requirements
KG-->>CC: Required docs + rules
CC->>PG: Get shipment documents + scores
PG-->>CC: Document data
CC->>LLM: Verify compliance per rule
LLM-->>CC: Compliance verdicts + citations
CC->>CC: Cross-reference sources
CC-->>S: Compliance Report
S->>PG: Store compliance report
S-->>W: Job Complete
Architecture:
graph LR
subgraph "Clerk"
ORG[Organizations<br/>= Tenants]
USR[Users]
ROLES[Roles<br/>Platform + System]
PERMS[Permissions<br/>Resource-Based]
end
subgraph "Platform Roles"
IMP[Importer]
EXP[Exporter]
CB[Custom_Broker]
end
subgraph "System Roles"
PA[Platform_Admin]
SA[Support_Agent]
DU[Demo_User]
end
ORG --> USR
USR --> ROLES
ROLES --> PERMS
ROLES --> IMP
ROLES --> EXP
ROLES --> CB
ROLES --> PA
ROLES --> SA
ROLES --> DU
RBAC Permission Matrix:
Permissions are checked against business roles (trade identity context) and system roles (platform capabilities). A user with multiple business roles gets the union of permissions for all their roles.
| Permission | Importer (business) | Exporter (business) | Custom_Broker (business) | Platform_Admin (system) | Support_Agent (system) | Demo_User (system) |
|---|---|---|---|---|---|---|
shipment:create |
✅ | ❌ | ✅ | ✅ | ❌ | ✅ (demo only) |
shipment:read |
Own | Linked | Assigned | All | All (read-only) | Demo only |
shipment:write |
Own | ❌ | Assigned | All | ❌ | Demo only |
document:upload |
Own | Via Magic Link | Assigned | All | ❌ | Demo only |
document:read |
Own | Linked | Assigned | All | All | Demo only |
compliance:run |
Own | ❌ | Assigned | All | ❌ | Demo only |
compliance:read |
Own | ❌ | Assigned | All | All | Demo only |
sanctions:review |
❌ | ❌ | ✅ | ✅ | ❌ | Demo only |
tariff:lookup |
Own | ❌ | Assigned | All | ❌ | Demo only |
magic_link:create |
✅ | ❌ | ✅ | ✅ | ❌ | Demo only |
supplier:search |
✅ | ❌ | ✅ | ✅ | ❌ | Demo only |
billing:manage |
✅ (org owner) | ❌ | ✅ (org owner) | ✅ | ❌ | ❌ |
admin:manage |
❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
demo:reset |
❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
audit:read |
Own | ❌ | Assigned | All | All | Demo only |
audit:export |
Own | ❌ | Assigned | All | ❌ | Demo only |
Context-Based Permission Resolution:
- When a user with both
importerandexporterbusiness roles creates a shipment, the system checks theimportercontext. - When the same user uploads documents via a magic link, the system checks the
exportercontext. - The API Gateway resolves the active business role from the request context (endpoint, action type, or explicit header).
Clerk Organization Mapping:
- Each tenant = 1 Clerk Organization
- Users belong to organizations with assigned roles
- Custom_Broker can be a member of multiple organizations (one per assigned Importer)
- Clerk JWT includes
org_id(tenant_id),org_role, and custompermissionsclaim - API Gateway extracts tenant context from JWT on every request
Architecture:
graph TB
subgraph "Demo System"
DT[Demo Tenant<br/>Isolated Clerk Org]
DD[Demo Data Seeder<br/>Pre-configured Shipments]
DR[Demo Reset Service<br/>Scheduled + On-Demand]
DF[Demo Feature Flags<br/>Premium Features Enabled]
DML[Demo Magic Links<br/>Pre-generated Exporter Portal]
end
subgraph "Demo Flow"
SALES[Sales Rep<br/>Sends Demo Link]
PROSPECT[Prospect<br/>Opens Demo]
PORTAL[Exporter Portal Demo<br/>Pre-seeded Documents]
DASH[Dashboard Demo<br/>Sample Readiness Data]
COMP[Compliance Demo<br/>Sample Check Results]
end
SALES --> PROSPECT
PROSPECT --> DML
DML --> PORTAL
PROSPECT --> DASH
PROSPECT --> COMP
DR -->|Reset every 24h| DT
DD -->|Seed data| DT
Demo Data Seed:
- 5 sample shipments across India→USA, USA→Canada, Canada→India corridors
- Pre-uploaded documents with varying readiness scores (Complete, Needs_Attention, Non_Compliant)
- Pre-run compliance checks with sample results
- 1 sample sanctions "Potential Hit" (false positive) for demonstration
- Pre-classified HS codes with tariff rates
- Sample audit trail entries
Demo Reset Process:
- Scheduled CRON job runs every 24 hours (configurable)
- On-demand reset via
POST /api/v1/admin/demo/reset(Platform_Admin only) - Reset steps:
- Delete all demo tenant data (shipments, documents, compliance results)
- Re-run demo data seeder
- Regenerate demo magic links
- Clear demo tenant cache in Redis
- Demo tenant identified by reserved
tenant_idin config
Demo Access Flow:
- Sales rep generates a magic link to the demo environment
- Prospect accesses demo with Demo_User role (read + limited write)
- All demo actions are sandboxed to the demo tenant
- Demo feature flags enable premium features for showcase
All tables follow these conventions:
id: UUID primary key (generated by application, not auto-increment)tenant_id: UUID foreign key totenantstable, present on every tenant-scoped tablecreated_at,updated_at: Timestamps with timezonecreated_by,updated_by: UUID references to the acting user- Row-Level Security (RLS) policies enforce
tenant_idfiltering at the database level
RLS Policy Template (applied to every tenant-scoped table):
ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON {table_name}
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);The API Gateway sets app.current_tenant_id on every database connection from the JWT claims.
erDiagram
TENANTS ||--o{ USERS : "belongs to"
TENANTS ||--o{ SHIPMENTS : "owns"
TENANTS ||--o{ ORGANIZATIONS : "has"
TENANTS ||--o{ SUBSCRIPTIONS : "has"
TENANTS ||--o{ USAGE_RECORDS : "tracks"
USERS ||--o{ SHIPMENTS : "creates"
USERS ||--o{ USER_BUSINESS_ROLES : "has business roles"
ORGANIZATIONS ||--o{ ORG_MEMBERS : "has"
USERS ||--o{ ORG_MEMBERS : "member of"
SHIPMENTS ||--o{ DOCUMENTS : "contains"
SHIPMENTS ||--o{ LINE_ITEMS : "contains"
SHIPMENTS ||--o{ COMPLIANCE_CHECKS : "undergoes"
SHIPMENTS ||--o{ SANCTIONS_SCREENINGS : "screened by"
SHIPMENTS ||--o{ MAGIC_LINKS : "has"
SHIPMENTS }o--|| TRADE_CORRIDORS : "uses"
DOCUMENTS ||--o{ DOCUMENT_VERSIONS : "has versions"
DOCUMENT_VERSIONS ||--o{ DOCUMENT_SCORES : "scored"
DOCUMENT_VERSIONS ||--o{ EXTRACTED_FIELDS : "contains"
LINE_ITEMS }o--o| HS_CODES : "classified as"
LINE_ITEMS ||--o{ HS_CODE_SUGGESTIONS : "has suggestions"
COMPLIANCE_CHECKS ||--o{ COMPLIANCE_RESULTS : "produces"
SANCTIONS_SCREENINGS ||--o{ SANCTIONS_HITS : "finds"
SANCTIONS_HITS ||--o| SANCTIONS_DISPOSITIONS : "resolved by"
SHIPMENTS ||--o{ NOTIFICATIONS : "triggers"
USERS ||--o{ NOTIFICATIONS : "receives"
TENANTS ||--o{ AUDIT_LOGS : "recorded in"
JOBS ||--o| SHIPMENTS : "processes"
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
clerk_org_id VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
plan VARCHAR(50) NOT NULL DEFAULT 'starter', -- starter, smb, professional, enterprise
stripe_customer_id VARCHAR(255) UNIQUE, -- Stripe Customer ID
is_demo BOOLEAN NOT NULL DEFAULT FALSE,
settings JSONB NOT NULL DEFAULT '{}', -- tenant-specific config
feature_flags JSONB NOT NULL DEFAULT '{}', -- enterprise-specific feature toggles
encryption_key_arn VARCHAR(512), -- AWS KMS key ARN
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, suspended, deleted
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
clerk_user_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
avatar_url VARCHAR(512),
system_role VARCHAR(20), -- platform_admin, support_agent, demo_user
is_active BOOLEAN NOT NULL DEFAULT TRUE,
last_login_at TIMESTAMPTZ,
failed_login_count INTEGER NOT NULL DEFAULT 0,
locked_until TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_users_clerk ON users(clerk_user_id);
CREATE INDEX idx_users_email ON users(email);A user can hold multiple business roles simultaneously (e.g., a wholesaler is both importer AND exporter). Business roles define a user's trade identity, separate from system/platform roles which define what they can do on the platform.
CREATE TABLE user_business_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
business_role VARCHAR(30) NOT NULL, -- importer, exporter, custom_broker, manufacturer, wholesaler
is_primary BOOLEAN DEFAULT FALSE,
tenant_id UUID NOT NULL REFERENCES tenants(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(user_id, business_role, tenant_id)
);
CREATE INDEX idx_user_business_roles_user ON user_business_roles(user_id);
CREATE INDEX idx_user_business_roles_tenant ON user_business_roles(tenant_id);
CREATE INDEX idx_user_business_roles_role ON user_business_roles(business_role);Role Model Design Rationale:
- Business roles (importer, exporter, custom_broker, manufacturer, wholesaler) define the user's trade identity — who they are in the trade ecosystem. A user can have multiple.
- System roles (platform_admin, support_agent, demo_user) define platform capabilities — what they can do on the platform. Stored on the
userstable as a single value. - The RBAC system checks permissions based on the context of the action: when creating a shipment, check if the user has the
importerbusiness role; when uploading via magic link, check theexportercontext. - This replaces the previous single
user_typecolumn which incorrectly assumed a user could only be one type.
CREATE TABLE broker_assignments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
broker_user_id UUID NOT NULL REFERENCES users(id),
importer_user_id UUID NOT NULL REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, revoked
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ,
created_by UUID REFERENCES users(id),
UNIQUE(broker_user_id, importer_user_id)
);
CREATE INDEX idx_broker_assignments_broker ON broker_assignments(broker_user_id);
CREATE INDEX idx_broker_assignments_importer ON broker_assignments(importer_user_id);CREATE TABLE shipments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
reference_number VARCHAR(100) NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'document_collection',
-- Status: document_collection, under_review, compliance_check,
-- sanctions_hold, cleared, archived
importer_id UUID NOT NULL REFERENCES users(id),
exporter_email VARCHAR(255) NOT NULL,
exporter_name VARCHAR(255),
exporter_user_id UUID REFERENCES users(id), -- linked after signup
custom_broker_id UUID REFERENCES users(id),
origin_country CHAR(2) NOT NULL, -- ISO 3166-1 alpha-2
destination_country CHAR(2) NOT NULL,
trade_corridor_id UUID REFERENCES trade_corridors(id),
total_value DECIMAL(15, 2),
currency_code CHAR(3) NOT NULL DEFAULT 'USD', -- ISO 4217
readiness_score DECIMAL(5, 2) DEFAULT 0.00, -- 0.00 to 100.00
compliance_status VARCHAR(20), -- pass, fail, warning, pending
sanctions_status VARCHAR(20) DEFAULT 'pending', -- clear, potential_hit, confirmed_hit, pending
checklist_generated_at TIMESTAMPTZ,
checklist_kg_version VARCHAR(50), -- Knowledge Graph version used
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID NOT NULL REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
CREATE INDEX idx_shipments_tenant ON shipments(tenant_id);
CREATE INDEX idx_shipments_importer ON shipments(importer_id);
CREATE INDEX idx_shipments_status ON shipments(status);
CREATE INDEX idx_shipments_corridor ON shipments(origin_country, destination_country);
CREATE INDEX idx_shipments_readiness ON shipments(readiness_score);CREATE TABLE trade_corridors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
origin_country CHAR(2) NOT NULL,
destination_country CHAR(2) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(origin_country, destination_country)
);CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
shipment_id UUID NOT NULL REFERENCES shipments(id),
document_type VARCHAR(50) NOT NULL,
-- Types: bill_of_lading, commercial_invoice, packing_list,
-- certificate_of_origin, customs_declaration,
-- letter_of_credit, purchase_order
is_required BOOLEAN NOT NULL DEFAULT TRUE,
condition_description TEXT, -- for conditionally required docs
current_version_id UUID, -- points to latest version
current_score VARCHAR(20), -- complete, needs_attention, non_compliant, missing
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
CREATE INDEX idx_documents_shipment ON documents(shipment_id);
CREATE INDEX idx_documents_tenant ON documents(tenant_id);
CREATE INDEX idx_documents_type ON documents(document_type);CREATE TABLE document_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
document_id UUID NOT NULL REFERENCES documents(id),
version_number INTEGER NOT NULL,
file_path VARCHAR(512) NOT NULL, -- S3 key
file_name VARCHAR(255) NOT NULL,
file_size_bytes BIGINT NOT NULL,
file_format VARCHAR(10) NOT NULL, -- pdf, png, jpg, tiff, csv, xlsx
file_hash_sha256 VARCHAR(64) NOT NULL,
uploaded_by UUID NOT NULL REFERENCES users(id),
uploaded_via VARCHAR(20) NOT NULL DEFAULT 'portal', -- portal, magic_link, api
scan_status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed
extracted_data JSONB, -- structured extraction result
extraction_confidence JSONB, -- per-field confidence scores
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(document_id, version_number)
);
CREATE INDEX idx_doc_versions_document ON document_versions(document_id);
CREATE INDEX idx_doc_versions_tenant ON document_versions(tenant_id);
CREATE INDEX idx_doc_versions_hash ON document_versions(file_hash_sha256);CREATE TABLE document_scores (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
document_version_id UUID NOT NULL REFERENCES document_versions(id),
score VARCHAR(20) NOT NULL, -- complete, needs_attention, non_compliant
issues JSONB NOT NULL DEFAULT '[]', -- [{field, issue, recommendation}]
field_scores JSONB NOT NULL DEFAULT '{}', -- {field_name: {score, confidence, issues}}
computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
computed_by VARCHAR(50) NOT NULL DEFAULT 'ai_service' -- ai_service, manual_override
);
CREATE INDEX idx_doc_scores_version ON document_scores(document_version_id);
CREATE INDEX idx_doc_scores_tenant ON document_scores(tenant_id);CREATE TABLE line_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
shipment_id UUID NOT NULL REFERENCES shipments(id),
source_document_id UUID REFERENCES documents(id), -- PO that contained this item
description TEXT NOT NULL,
quantity DECIMAL(15, 4),
unit_of_measure VARCHAR(20),
unit_price DECIMAL(15, 4),
currency_code CHAR(3),
total_value DECIMAL(15, 2),
hs_code VARCHAR(12), -- confirmed HS code
hs_code_status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, suggested, confirmed, manual
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by UUID REFERENCES users(id),
updated_by UUID REFERENCES users(id)
);
CREATE INDEX idx_line_items_shipment ON line_items(shipment_id);
CREATE INDEX idx_line_items_tenant ON line_items(tenant_id);
CREATE INDEX idx_line_items_hs ON line_items(hs_code);CREATE TABLE hs_code_suggestions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
line_item_id UUID NOT NULL REFERENCES line_items(id),
suggested_hs_code VARCHAR(12) NOT NULL,
confidence_score DECIMAL(3, 2) NOT NULL, -- 0.00 to 1.00
rank INTEGER NOT NULL, -- 1, 2, 3
explanation TEXT NOT NULL,
source_references JSONB NOT NULL DEFAULT '[]', -- KG + RAG sources
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, accepted, rejected
reviewed_by UUID REFERENCES users(id),
reviewed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_hs_suggestions_item ON hs_code_suggestions(line_item_id);CREATE TABLE compliance_checks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
shipment_id UUID NOT NULL REFERENCES shipments(id),
initiated_by UUID NOT NULL REFERENCES users(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed
overall_result VARCHAR(20), -- pass, fail, warning
kg_version VARCHAR(50) NOT NULL, -- Knowledge Graph version at check time
report_pdf_path VARCHAR(512), -- S3 key for PDF report
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_compliance_checks_shipment ON compliance_checks(shipment_id);
CREATE INDEX idx_compliance_checks_tenant ON compliance_checks(tenant_id);CREATE TABLE compliance_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
compliance_check_id UUID NOT NULL REFERENCES compliance_checks(id),
check_type VARCHAR(50) NOT NULL, -- document_completeness, field_validation, regulatory_compliance
rule_reference VARCHAR(255) NOT NULL, -- regulation citation
result VARCHAR(20) NOT NULL, -- pass, fail, warning
description TEXT NOT NULL,
source_regulation TEXT, -- full regulation text
source_url VARCHAR(512), -- link to regulation
remediation_action TEXT, -- recommended fix
cross_reference_sources JSONB DEFAULT '[]', -- independent verification sources
discrepancy_notes TEXT, -- if sources disagree
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_compliance_results_check ON compliance_results(compliance_check_id);CREATE TABLE sanctions_screenings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
shipment_id UUID NOT NULL REFERENCES shipments(id),
screened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
sanctions_data_version VARCHAR(50) NOT NULL, -- version of sanctions data used
overall_result VARCHAR(20) NOT NULL, -- clear, potential_hit
parties_screened JSONB NOT NULL, -- [{name, role, result}]
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sanctions_screenings_shipment ON sanctions_screenings(shipment_id);CREATE TABLE sanctions_hits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
screening_id UUID NOT NULL REFERENCES sanctions_screenings(id),
shipment_id UUID NOT NULL REFERENCES shipments(id),
screened_name VARCHAR(255) NOT NULL,
screened_role VARCHAR(50) NOT NULL, -- importer, exporter, consignee, notify_party
matched_entity_name VARCHAR(255) NOT NULL,
matched_entity_id VARCHAR(100),
source_list VARCHAR(20) NOT NULL, -- ofac_sdn, eu_consolidated, un_consolidated
similarity_score DECIMAL(5, 4) NOT NULL, -- 0.0000 to 1.0000
match_details JSONB NOT NULL DEFAULT '{}',
status VARCHAR(20) NOT NULL DEFAULT 'pending_review', -- pending_review, confirmed_match, false_positive
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_sanctions_hits_shipment ON sanctions_hits(shipment_id);
CREATE INDEX idx_sanctions_hits_status ON sanctions_hits(status);CREATE TABLE sanctions_dispositions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
sanctions_hit_id UUID NOT NULL REFERENCES sanctions_hits(id) UNIQUE,
disposition VARCHAR(20) NOT NULL, -- confirmed_match, false_positive
justification TEXT NOT NULL,
reviewed_by UUID NOT NULL REFERENCES users(id),
reviewed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);CREATE TABLE magic_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
shipment_id UUID NOT NULL REFERENCES shipments(id),
token VARCHAR(128) UNIQUE NOT NULL, -- cryptographically secure token
exporter_email VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
is_used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ,
reminder_sent_at TIMESTAMPTZ,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_magic_links_token ON magic_links(token);
CREATE INDEX idx_magic_links_shipment ON magic_links(shipment_id);
CREATE INDEX idx_magic_links_expires ON magic_links(expires_at);CREATE TABLE jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
bullmq_job_id VARCHAR(255),
job_type VARCHAR(50) NOT NULL, -- document_scan, compliance_check, sanctions_screening, hs_classification
status VARCHAR(20) NOT NULL DEFAULT 'queued', -- queued, processing, completed, failed
priority INTEGER NOT NULL DEFAULT 0, -- higher = more priority
input_data JSONB NOT NULL,
result_data JSONB,
error_message TEXT,
retry_count INTEGER NOT NULL DEFAULT 0,
max_retries INTEGER NOT NULL DEFAULT 3,
shipment_id UUID REFERENCES shipments(id),
document_version_id UUID REFERENCES document_versions(id),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_jobs_tenant ON jobs(tenant_id);
CREATE INDEX idx_jobs_status ON jobs(status);
CREATE INDEX idx_jobs_type ON jobs(job_type);
CREATE INDEX idx_jobs_shipment ON jobs(shipment_id);CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
recipient_user_id UUID REFERENCES users(id),
recipient_email VARCHAR(255), -- for non-user recipients (exporters)
notification_type VARCHAR(50) NOT NULL,
-- Types: document_uploaded, score_changed, sanctions_hit,
-- magic_link_reminder, compliance_complete, checklist_updated
channel VARCHAR(20) NOT NULL DEFAULT 'email', -- email (MVP), in_app, sms (future)
subject VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
reference_type VARCHAR(50), -- shipment, document, compliance_check
reference_id UUID,
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, sent, failed
sent_at TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_notifications_recipient ON notifications(recipient_user_id);
CREATE INDEX idx_notifications_status ON notifications(status);
CREATE INDEX idx_notifications_tenant ON notifications(tenant_id);CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL, -- no FK to allow tenant deletion
actor_id UUID, -- user who performed action (null for system)
actor_type VARCHAR(20) NOT NULL, -- user, system, magic_link, webhook
actor_email VARCHAR(255),
actor_ip INET,
actor_user_agent TEXT,
action VARCHAR(100) NOT NULL,
-- Actions: auth.login, auth.logout, auth.failed, auth.locked,
-- shipment.create, shipment.update, document.upload,
-- document.scan, compliance.check, compliance.report,
-- sanctions.screen, sanctions.review, hs_code.classify,
-- hs_code.confirm, magic_link.create, magic_link.use,
-- tariff.lookup, admin.demo_reset, rate_limit.exceeded,
-- data.access_denied
resource_type VARCHAR(50), -- shipment, document, compliance_check, etc.
resource_id UUID,
old_value JSONB, -- previous state (for updates)
new_value JSONB, -- new state (for creates/updates)
metadata JSONB NOT NULL DEFAULT '{}', -- additional context
source_regulation VARCHAR(255), -- for compliance decisions
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Append-only: no UPDATE or DELETE allowed
REVOKE UPDATE, DELETE ON audit_logs FROM app_user;
-- Indexes for audit queries
CREATE INDEX idx_audit_tenant ON audit_logs(tenant_id);
CREATE INDEX idx_audit_actor ON audit_logs(actor_id);
CREATE INDEX idx_audit_action ON audit_logs(action);
CREATE INDEX idx_audit_resource ON audit_logs(resource_type, resource_id);
CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp);
-- Partition by month for performance
CREATE TABLE audit_logs_partitioned (LIKE audit_logs INCLUDING ALL)
PARTITION BY RANGE (timestamp);CREATE TABLE data_ingestion_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source VARCHAR(50) NOT NULL, -- ofac_sdn, eu_sanctions, un_sanctions, usitc_hts, etc.
fetch_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
record_count INTEGER,
status VARCHAR(20) NOT NULL, -- success, partial, failed
error_message TEXT,
retry_count INTEGER NOT NULL DEFAULT 0,
data_version VARCHAR(50), -- version identifier for this fetch
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_ingestion_source ON data_ingestion_logs(source);
CREATE INDEX idx_ingestion_status ON data_ingestion_logs(status);CREATE TABLE supplier_network (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) UNIQUE,
company_name VARCHAR(255),
country CHAR(2),
verified BOOLEAN NOT NULL DEFAULT FALSE,
profile_data JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_supplier_company ON supplier_network(company_name);
CREATE INDEX idx_supplier_country ON supplier_network(country);CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
stripe_subscription_id VARCHAR(255) UNIQUE NOT NULL,
stripe_price_id VARCHAR(255) NOT NULL,
plan VARCHAR(50) NOT NULL, -- starter, smb, professional, enterprise
status VARCHAR(30) NOT NULL, -- active, past_due, canceled, trialing, incomplete
current_period_start TIMESTAMPTZ NOT NULL,
current_period_end TIMESTAMPTZ NOT NULL,
cancel_at_period_end BOOLEAN NOT NULL DEFAULT FALSE,
canceled_at TIMESTAMPTZ,
trial_end TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_subscriptions_tenant ON subscriptions(tenant_id);
CREATE INDEX idx_subscriptions_stripe ON subscriptions(stripe_subscription_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);CREATE TABLE usage_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
subscription_id UUID NOT NULL REFERENCES subscriptions(id),
usage_type VARCHAR(50) NOT NULL, -- shipment_created, document_scan, compliance_check, sanctions_screening, hs_classification
quantity INTEGER NOT NULL DEFAULT 1,
stripe_usage_record_id VARCHAR(255), -- Stripe metered billing record ID
reference_type VARCHAR(50), -- shipment, document, compliance_check
reference_id UUID,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
billing_period_start TIMESTAMPTZ NOT NULL,
billing_period_end TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_usage_records_tenant ON usage_records(tenant_id);
CREATE INDEX idx_usage_records_subscription ON usage_records(subscription_id);
CREATE INDEX idx_usage_records_type ON usage_records(usage_type);
CREATE INDEX idx_usage_records_period ON usage_records(billing_period_start, billing_period_end);Node Types:
// Country node
(:Country {
code: "US", // ISO 3166-1 alpha-2
name: "United States",
region: "North America",
customs_authority: "CBP",
data_source_url: "https://..."
})
// HS Code node
(:HSCode {
code: "8471.30", // HS code (2-10 digits)
description: "Portable digital automatic data processing machines",
level: 6, // 2, 4, 6, 8, 10 digit levels
chapter: 84,
section: "XVI",
effective_date: date("2024-01-01"),
source: "usitc_hts"
})
// Tariff Rate node
(:TariffRate {
id: "uuid",
rate_type: "general", // general, preferential, anti_dumping, countervailing
rate_value: 2.5, // percentage
rate_unit: "percent", // percent, specific (per unit)
specific_rate: null, // for specific duties
effective_date: date("2024-01-01"),
expiry_date: null,
source_regulation: "HTS Chapter 84, Subheading 8471.30",
source_url: "https://...",
version: "2024.1",
last_updated: datetime()
})
// Trade Agreement node
(:TradeAgreement {
id: "uuid",
name: "USMCA",
full_name: "United States-Mexico-Canada Agreement",
effective_date: date("2020-07-01"),
status: "active",
source_url: "https://..."
})
// Regulation node
(:Regulation {
id: "uuid",
title: "Certificate of Origin Requirements",
body: "...",
regulation_code: "19 CFR 10.411",
category: "documentation", // documentation, product_restriction, labeling, country_specific
effective_date: date("2024-01-01"),
expiry_date: null,
source_url: "https://...",
version: "2024.1",
last_updated: datetime()
})
// Document Type node
(:DocumentType {
code: "certificate_of_origin",
name: "Certificate of Origin",
description: "...",
required_fields: ["exporter_name", "importer_name", "goods_description", "hs_code", "origin_criteria"]
})
// Sanctioned Entity node
(:SanctionedEntity {
id: "uuid",
name: "...",
aliases: ["...", "..."],
entity_type: "individual", // individual, organization, vessel
source_list: "ofac_sdn", // ofac_sdn, eu_consolidated, un_consolidated
source_id: "...", // ID from source list
country: "IR",
programs: ["IRAN", "SDGT"],
effective_date: date("2024-01-01"),
last_updated: datetime(),
data_version: "2024-06-15"
})
// Product Category node (HS Code Sections/Chapters)
(:ProductCategory {
id: "uuid",
code: "30", // HS Chapter number
section: "VI", // HS Section (Roman numeral)
name: "Pharmaceutical Products",
description: "Pharmaceutical products including drugs, medicines, and medical preparations",
risk_level: "high", // low, medium, high, critical
special_requirements: ["drug_license", "gmp_certificate", "cold_chain_documentation"],
metadata: {}
})
// Regulatory Body node
(:RegulatoryBody {
id: "uuid",
code: "FDA",
name: "Food and Drug Administration",
country: "US",
jurisdiction: "federal", // federal, state, international
website: "https://www.fda.gov",
data_source_url: "https://...",
categories_governed: ["30", "01-24"], // HS chapters
metadata: {}
})Product Category — Regulated Categories:
| HS Chapter(s) | Category | Key Requirements | Regulatory Bodies |
|---|---|---|---|
| 30 | Pharmaceuticals | Drug licenses, GMP certificates, FDA/CDSCO approvals, cold chain docs | FDA (US), CDSCO (IN), Health Canada (CA) |
| 1-24 | Food & Agriculture | Phytosanitary certificates, FSSAI/FDA food safety, fumigation, health certs | FDA (US), FSSAI (IN), CFIA (CA) |
| 93 | Arms & Ammunition | Export licenses, end-user certificates, ITAR compliance | DDTC (US), DGFT (IN), GAC (CA) |
| 28-29 | Chemicals | MSDS, hazardous goods declaration, REACH compliance | EPA (US), CPCB (IN), ECCC (CA) |
| 71 | Precious Metals/Stones | Kimberley Process certificates, hallmarking | CBP (US), BIS (IN), CBSA (CA) |
| 50-63 | Textiles | Country of origin labeling, quota compliance | CBP (US), DGFT (IN), CBSA (CA) |
Relationship Types:
// Trade corridor relationships
(:Country)-[:HAS_CORRIDOR {is_active: true}]->(:Country)
// HS Code hierarchy
(:HSCode)-[:PARENT_OF]->(:HSCode)
// Tariff rates for HS codes in corridors
(:HSCode)-[:HAS_TARIFF_RATE {
origin: "IN", destination: "US"
}]->(:TariffRate)
// Trade agreement applicability
(:TradeAgreement)-[:APPLIES_TO_CORRIDOR {
origin: "US", destination: "CA"
}]->(:Country)
(:TradeAgreement)-[:GRANTS_PREFERENTIAL_RATE]->(:TariffRate)
// Document requirements
(:HSCode)-[:REQUIRES_DOCUMENT {
corridor_origin: "IN",
corridor_destination: "US",
is_mandatory: true,
condition: null
}]->(:DocumentType)
// Regulation governance
(:Regulation)-[:GOVERNS_CORRIDOR {
origin: "IN", destination: "US"
}]->(:Country)
(:Regulation)-[:APPLIES_TO_HS_CODE]->(:HSCode)
(:Regulation)-[:REQUIRES_DOCUMENT]->(:DocumentType)
// Sanctions relationships
(:SanctionedEntity)-[:ASSOCIATED_WITH]->(:Country)
(:SanctionedEntity)-[:ALIAS_OF]->(:SanctionedEntity)
// Product category relationships
(:HSCode)-[:BELONGS_TO_CATEGORY]->(:ProductCategory)
(:ProductCategory)-[:REGULATED_BY {
country: "US",
requirement_type: "mandatory"
}]->(:RegulatoryBody)
(:RegulatoryBody)-[:OPERATES_IN]->(:Country)
(:ProductCategory)-[:REQUIRES_CATEGORY_DOCUMENT {
corridor_origin: "IN",
corridor_destination: "US",
is_mandatory: true,
condition: "Required for all pharmaceutical imports"
}]->(:DocumentType)
(:RegulatoryBody)-[:ISSUES_DOCUMENT]->(:DocumentType)Version Control for Knowledge Graph:
// Every data node has version metadata
// When data is updated, old node gets expiry_date, new node is created
// Compliance checks record the KG version they used
(:KGVersion {
version: "2024.26", // year.week
created_at: datetime(),
sources_updated: ["ofac_sdn", "usitc_hts"],
change_summary: "OFAC SDN daily update + HTS 2024 revision"
})
(:KGVersion)-[:INCLUDES_UPDATE]->(:DataIngestionLog)-- Regulatory text embeddings for RAG
CREATE TABLE regulatory_embeddings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_type VARCHAR(50) NOT NULL, -- regulation, trade_ruling, document_template
source_id VARCHAR(255) NOT NULL,
source_url VARCHAR(512),
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
content_chunk TEXT NOT NULL, -- chunked text for embedding
chunk_index INTEGER NOT NULL,
embedding vector(768) NOT NULL, -- nomic-embed-text / Titan dimension
country_codes CHAR(2)[], -- applicable countries
hs_code_prefixes VARCHAR(12)[], -- applicable HS code prefixes
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_reg_embeddings_source ON regulatory_embeddings(source_type, source_id);
CREATE INDEX idx_reg_embeddings_vector ON regulatory_embeddings
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);# Rate limiting (sliding window)
rate_limit:user:{user_id} -> Sorted Set (timestamp scores)
rate_limit:ip:{ip_address} -> Sorted Set (timestamp scores)
rate_limit:magic_link:{shipment_id} -> Counter with TTL
# Tariff rate cache
tariff:{hs_code}:{origin}:{dest} -> JSON string (TTL: 24h)
# Job status (real-time)
job:status:{job_id} -> Hash {status, progress, result_url}
# Session/notification pub-sub
notifications:{tenant_id}:{user_id} -> Pub/Sub channel
# Demo tenant tracking
demo:tenant:{tenant_id}:last_reset -> Timestamp
demo:tenant:{tenant_id}:seed_status -> String
# BullMQ queues
bull:document-processing -> BullMQ queue
bull:compliance-check -> BullMQ queue (higher priority)
bull:sanctions-screening -> BullMQ queue (highest priority)
bull:hs-classification -> BullMQ queue
bull:data-ingestion -> BullMQ queue
bull:notifications -> BullMQ queue
graph TB
subgraph "Data Sources"
OFAC[OFAC SDN<br/>Daily CSV/XML]
EU[EU Sanctions<br/>Regular XML]
UN[UN Sanctions<br/>Regular XML]
HTS[USITC HTS<br/>Weekly]
CAN[Canada Tariff<br/>Weekly]
IND[India DGFT<br/>Weekly]
end
subgraph "Ingestion Pipeline"
SCHED[Scheduler<br/>Configurable Cron]
ADAPT[Country Adapters<br/>Pluggable per Source]
VALID[Schema Validator<br/>Reject Invalid Records]
DIFF[Change Detector<br/>Diff Against Existing]
end
subgraph "Knowledge Graph Update"
VER[Version Manager<br/>Create New KG Version]
NEO_UP[Neo4j Writer<br/>Upsert Nodes/Relationships]
SNAP[Snapshot Recorder<br/>Log Ingestion Details]
end
subgraph "Notification"
IMPACT[Impact Analyzer<br/>Find Affected Shipments]
NOTIFY[Notification Service<br/>Alert Affected Users]
end
OFAC --> SCHED
EU --> SCHED
UN --> SCHED
HTS --> SCHED
CAN --> SCHED
IND --> SCHED
SCHED --> ADAPT
ADAPT --> VALID
VALID --> DIFF
DIFF --> VER
VER --> NEO_UP
VER --> SNAP
DIFF --> IMPACT
IMPACT --> NOTIFY
Adding a New Country:
- Create a new adapter in
ai_service/ingestion/adapters/implementing the base adapter interface - Configure the data source URL and refresh schedule
- Ingest initial data into the Knowledge Graph
- Add trade corridor entries for the new country pairs
- No application code changes required — the country-agnostic data model handles the rest
| Feature | Starter (Free) | SMB | Professional | Enterprise |
|---|---|---|---|---|
| Price | $0/month | $45/month | $99/month | Custom pricing |
| Shipments/month | 5 | 25 | 100 | Unlimited |
| Document scans/month | 20 | 150 | 500 | Unlimited |
| Compliance checks/month | 5 | 25 | 100 | Unlimited |
| Sanctions screening | ❌ | ✅ (basic) | ✅ (full) | ✅ (full + re-screening) |
| Tariff lookup | Basic (cached only) | Full | Full | Full + historical |
| HS code classification | 10/month | 50/month | 200/month | Unlimited |
| Trade corridors | 2 | All MVP corridors | All MVP corridors | All + custom |
| Magic links/shipment | 3 | 5 | 10 | Unlimited |
| Document versioning | Latest 3 versions | Latest 10 versions | Full history | Full history |
| Audit trail export | ❌ | ✅ (PDF) | ✅ (PDF) | ✅ (PDF + CSV + API) |
| SSO (SAML/OIDC) | ❌ | ❌ | ❌ | ✅ |
| Dedicated support | Community | Email (72h SLA) | Email (48h SLA) | Dedicated CSM (4h SLA) |
| Custom integrations | ❌ | ❌ | ❌ | ✅ (API + webhooks) |
| Data retention | 90 days | 1 year | 2 years | Custom |
| Users/org | 2 | 5 | 15 | Unlimited |
| Supplier network access | ❌ | ✅ | ✅ | ✅ |
Baseline (0-50 tenants, early stage):
| Service | Spec | Monthly Cost |
|---|---|---|
| ECS Fargate — Next.js | 0.5 vCPU, 1GB RAM | ~$15 |
| ECS Fargate — NestJS Gateway | 1 vCPU, 2GB RAM | ~$30 |
| ECS Fargate — FastAPI AI Service | 1 vCPU, 4GB RAM | ~$55 |
| ECS Fargate — BullMQ Workers | 0.5 vCPU, 2GB RAM | ~$25 |
| RDS PostgreSQL (db.t4g.medium) | 2 vCPU, 4GB RAM, 100GB gp3 | ~$70 |
| ElastiCache Redis (cache.t4g.micro) | 1 node | ~$15 |
| Neo4j (EC2 t3.medium) | 2 vCPU, 4GB RAM, 50GB | ~$35 |
| S3 (document storage) | 50GB + requests | ~$5 |
| ALB | 1 load balancer | ~$20 |
| CloudFront | 100GB transfer | ~$10 |
| AWS SES | 10K emails/month | ~$1 |
| AWS Textract | ~500 pages/month | ~$8 |
| AWS Bedrock (AI inference) | See breakdown below | ~$50-150 |
| CloudWatch + WAF | Logs, metrics, firewall | ~$25 |
| KMS | 5 keys + requests | ~$5 |
| Total baseline | ~$370-470/month |
Bedrock AI Cost Breakdown (per 1000 document scans):
| Model | Task | Tokens/scan | Cost/1K scans |
|---|---|---|---|
| Nova Micro ($0.035/$0.14 per M) | Agent routing, classification | ~500 in + 200 out | ~$0.05 |
| Nova Lite ($0.06/$0.24 per M) | Field extraction | ~2K in + 1K out | ~$0.36 |
| Nova Pro ($0.80/$3.20 per M) | HS classification, doc understanding | ~3K in + 1K out | ~$5.60 |
| Claude Sonnet 4.5 ($3/$15 per M) | Compliance analysis (only when needed) | ~4K in + 2K out | ~$42.00 |
| Titan Embeddings ($0.02 per M) | RAG embeddings | ~1K per chunk | ~$0.02 |
Key insight: By routing 80% of tasks to Nova Micro/Lite (pennies), and only using Claude 4.5 for actual compliance analysis (~10% of tasks), the average AI cost per document scan is approximately $0.05-0.15. At the SMB plan ($45/month, 150 scans), AI cost is ~$7.50-22.50, leaving healthy margin.
Scaling estimate (500 tenants):
- Infrastructure scales to ~$1,500-2,500/month
- AI inference scales linearly with usage
- Neo4j may need upgrade to dedicated instance (~$200/month)
- RDS may need Multi-AZ (~$140/month)
graph TB
subgraph "NestJS API Gateway"
BC[Billing Controller<br/>Subscription CRUD]
BWH[Stripe Webhook Controller<br/>Event Processing]
PLM[Plan Limits Middleware<br/>Usage Enforcement]
end
subgraph "Billing Service"
BS[Billing Service<br/>Stripe SDK Integration]
UT[Usage Tracker<br/>Metered Billing]
PE[Plan Enforcer<br/>Limit Checks]
end
subgraph "Stripe"
SC[Stripe Customers]
SS[Stripe Subscriptions<br/>Stripe Billing]
SP[Stripe Prices<br/>Standard + Custom]
SU[Stripe Usage Records<br/>Metered Billing]
SWH[Stripe Webhooks]
SBP[Stripe Billing Portal<br/>Self-Service]
end
subgraph "Database"
SUB[(subscriptions table)]
UR[(usage_records table)]
TEN[(tenants table<br/>stripe_customer_id)]
end
BC --> BS
BWH --> BS
PLM --> PE
BS --> SC
BS --> SS
BS --> SP
UT --> SU
BS --> SUB
UT --> UR
BS --> TEN
SWH --> BWH
BC --> SBP
Subscription Lifecycle (Webhook Events):
| Stripe Event | Platform Action |
|---|---|
customer.subscription.created |
Create subscriptions record, update tenant plan |
customer.subscription.updated |
Update plan tier, adjust limits |
customer.subscription.deleted |
Downgrade to Starter, disable premium features |
invoice.payment_succeeded |
Record payment, reset monthly usage counters |
invoice.payment_failed |
Send notification, grace period (7 days), then suspend |
customer.subscription.trial_will_end |
Send trial ending notification (3 days before) |
Usage-Based Billing (Metered):
AI processing operations are tracked as metered usage and reported to Stripe:
- Document scans (per document processed by AI)
- Compliance checks (per shipment compliance run)
- HS code classifications (per item classified)
- Sanctions screenings (per shipment screened)
Usage is reported to Stripe at the end of each billing period via stripe.subscriptionItems.createUsageRecord().
Plan Limits Enforcement:
// plan-limits.middleware.ts
@Injectable()
export class PlanLimitsMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
const tenant = req.tenant;
const action = this.resolveAction(req);
const limits = PLAN_LIMITS[tenant.plan];
const currentUsage = await this.usageService.getCurrentPeriodUsage(
tenant.id, action
);
if (currentUsage >= limits[action]) {
throw new HttpException(
{
error: 'Plan limit exceeded',
limit: limits[action],
current: currentUsage,
upgradeUrl: `/billing/upgrade`,
},
HttpStatus.TOO_MANY_REQUESTS,
);
}
next();
}
}
const PLAN_LIMITS = {
starter: {
shipments_per_month: 5,
document_scans_per_month: 20,
compliance_checks_per_month: 5,
hs_classifications_per_month: 10,
magic_links_per_shipment: 3,
},
professional: {
shipments_per_month: 100,
document_scans_per_month: 500,
compliance_checks_per_month: 100,
hs_classifications_per_month: 200,
magic_links_per_shipment: 10,
},
enterprise: {
// Unlimited — enforced via feature flags, not hard limits
shipments_per_month: Infinity,
document_scans_per_month: Infinity,
compliance_checks_per_month: Infinity,
hs_classifications_per_month: Infinity,
magic_links_per_shipment: Infinity,
},
};Enterprise Custom Plans:
- Custom pricing negotiated by sales team
- Stripe Price API creates per-tenant custom prices (
stripe.prices.create()withlookup_keyper tenant) - Feature flags stored in
tenants.feature_flagsJSONB column for enterprise-specific capabilities (SSO, custom integrations, extended data retention) - Custom SLA terms stored in
tenants.settingsJSONB
Compliance requirements vary significantly by product category. HS codes are organized into 21 Sections and 97 Chapters, each with distinct regulatory requirements. The Document Checklist generation must consider both the trade corridor AND the product category to produce accurate requirements.
graph TB
subgraph "Input"
HS[Confirmed HS Codes]
TC[Trade Corridor<br/>Origin → Destination]
end
subgraph "Knowledge Graph Lookup"
CAT[Resolve Product Category<br/>HS Chapter → Category]
CORR[Corridor Requirements<br/>Country-pair rules]
CATREQ[Category Requirements<br/>Category-specific docs]
REG[Regulatory Bodies<br/>Applicable authorities]
end
subgraph "Checklist Generation"
MERGE[Merge Requirements<br/>Corridor ∪ Category]
DEDUP[Deduplicate & Prioritize<br/>Mandatory > Conditional]
GEN[Generate Document Checklist<br/>With conditions & regulatory refs]
end
HS --> CAT
TC --> CORR
CAT --> CATREQ
CAT --> REG
CORR --> MERGE
CATREQ --> MERGE
REG --> MERGE
MERGE --> DEDUP
DEDUP --> GEN
This is the most critical data question: there is no single downloadable database that maps "HS code + country → required documents." This mapping is our core intellectual property and competitive moat. Here's the hybrid approach — combining one-time LLM-assisted extraction with structured rule maintenance:
Building scrapers for every government website is a bad strategy because:
- Government sites change layout/structure without notice, breaking scrapers
- Many sites (India CBIC, ICEGATE) use JSP/dynamic rendering that's hard to scrape reliably
- Regulatory text requires interpretation, not just extraction — "items under Chapter 30 require drug license" needs to be mapped to specific HS codes
- Scraping frequency doesn't match regulation change frequency (regulations change monthly, not daily)
- Legal risk — some government sites have terms prohibiting automated scraping
Instead of scraping live sites repeatedly, we use a one-time bulk extraction + ongoing change monitoring approach:
graph TB
subgraph "Phase A — Initial Knowledge Base Build (One-Time)"
GOV[Government Source Documents<br/>trade.gov guides, CBIC manuals,<br/>CBSA D-Memoranda, DGFT FTP]
DOWNLOAD[Download/Save Source Documents<br/>PDF, HTML → local storage]
CHUNK[Chunk Documents<br/>By section, chapter, regulation]
LLM_EXTRACT[LLM Extraction Agent<br/>Claude 4.5 / Nova Pro<br/>Extract structured rules]
SCHEMA[Map to Knowledge Graph Schema<br/>Country → HS Chapter → Documents<br/>+ conditions + weights + regulatory body]
REVIEW[Human Expert Review<br/>Validate extracted rules<br/>Fix errors, add nuance]
KG_WRITE[Write to Knowledge Graph<br/>Versioned, with source citations]
end
subgraph "Phase B — Ongoing Change Monitoring"
RSS[RSS/Gazette Feeds<br/>CBIC notifications, CBP bulletins,<br/>DGFT public notices]
DIFF_AGENT[Change Detection Agent<br/>Compare new doc vs existing rules]
DRAFT[Draft KG Update<br/>LLM proposes rule changes]
CURATOR[Curator Review Queue<br/>Human approves/rejects changes]
UPDATE[Apply Approved Updates<br/>Version bump in KG]
end
GOV --> DOWNLOAD
DOWNLOAD --> CHUNK
CHUNK --> LLM_EXTRACT
LLM_EXTRACT --> SCHEMA
SCHEMA --> REVIEW
REVIEW --> KG_WRITE
RSS --> DIFF_AGENT
DIFF_AGENT --> DRAFT
DRAFT --> CURATOR
CURATOR --> UPDATE
Step 1: Source Document Collection (manual, one-time)
| Country | Source | What We Download | Format |
|---|---|---|---|
| USA | trade.gov Country Commercial Guides (all countries) | Import requirements per destination country | HTML → save as text |
| USA | CBP "Importing Into the United States" guide | General import documentation requirements | PDF (official publication) |
| USA | FDA Import Program (by product) | Food, drug, device, cosmetic import requirements | HTML pages per product category |
| USA | USDA APHIS import requirements | Plant/animal product requirements | HTML/PDF |
| India | DGFT Foreign Trade Policy 2023 + Handbook of Procedures | Import/export policy conditions by HS chapter | PDF (official publication) |
| India | CBIC Customs Manual | Clearance documentation requirements | |
| India | ICEGATE Single Window Document Code Map | Document codes required per clearance type | PDF (available on icegate.gov.in) |
| India | FSSAI import regulations | Food product import requirements | PDF/HTML |
| Canada | CBSA D-Memoranda series | Customs documentation by topic | HTML/PDF |
| Canada | CFIA import requirements | Food/plant/animal import requirements | HTML |
| Canada | Health Canada import guidance | Drug/device/cosmetic requirements | HTML/PDF |
Step 2: LLM-Assisted Extraction
We feed each source document to an extraction agent (Claude 4.5 in production, Llama 3.1 locally) with a structured prompt:
EXTRACTION_PROMPT = """
You are a trade compliance expert. Extract document requirements from the following
regulatory text into structured rules.
For each rule, provide:
1. applicable_hs_chapters: list of HS chapters (2-digit) this applies to, or "ALL"
2. applicable_hs_codes: specific HS codes if mentioned (4-6 digit), or null
3. origin_country: ISO 2-letter code, or "ALL"
4. destination_country: ISO 2-letter code
5. document_type: one of [commercial_invoice, bill_of_lading, packing_list,
certificate_of_origin, customs_declaration, letter_of_credit, purchase_order,
phytosanitary_certificate, health_certificate, drug_license, gmp_certificate,
fumigation_certificate, msds, hazardous_goods_declaration, fda_prior_notice,
fssai_import_license, bis_registration, import_permit, end_user_certificate,
test_report, inspection_certificate, OTHER:<specify>]
6. is_mandatory: true/false
7. condition: null or text describing when this document is required
8. regulatory_body: which authority requires this (e.g., FDA, FSSAI, CBP, CBSA)
9. regulation_reference: specific regulation citation
10. weight: critical (3.0) / important (2.0) / standard (1.0)
- critical: shipment CANNOT clear customs without this
- important: may cause delays or penalties if missing
- standard: good practice, may be requested during audit
11. product_category: specific category if applicable (pharma, food, chemicals, etc.)
12. source_url: URL of the source document
Output as JSON array.
REGULATORY TEXT:
{document_text}
"""Step 3: Human Expert Review
The LLM output goes into a curation queue where a trade compliance expert:
- Validates each extracted rule against their domain knowledge
- Fixes any misinterpretations (LLMs sometimes confuse "recommended" with "required")
- Adds nuance the LLM missed (e.g., "this only applies to shipments over $2,500")
- Assigns confidence scores to each rule
- Approves rules for Knowledge Graph insertion
Step 4: Knowledge Graph Population
Approved rules are written to Neo4j with full provenance:
// Example: FDA Prior Notice required for food imports to USA
CREATE (rule:DocumentRule {
id: "rule-usa-food-fda-prior-notice",
document_type: "fda_prior_notice",
is_mandatory: true,
condition: "Required for ALL food products (HS Chapters 1-24) imported into the USA",
weight: 3.0,
regulatory_body: "FDA",
regulation_reference: "21 CFR Part 1, Subpart I — Prior Notice of Imported Food",
source_url: "https://www.fda.gov/food/importing-food-products-united-states/prior-notice-imported-foods",
extraction_method: "llm_assisted",
reviewed_by: "expert_user_id",
reviewed_at: datetime(),
confidence: 0.95,
kg_version: "2026.01"
})
// Link to applicable HS chapters
MATCH (hs:HSCode) WHERE hs.chapter IN [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24]
MATCH (dest:Country {code: "US"})
CREATE (hs)-[:REQUIRES_DOCUMENT {
corridor_destination: "US",
corridor_origin: "ALL",
is_mandatory: true,
condition: "All food products",
weight: 3.0
}]->(rule)
// Link to product category
MATCH (cat:ProductCategory {code: "food_agriculture"})
CREATE (cat)-[:REQUIRES_CATEGORY_DOCUMENT]->(rule)Change Sources (automated monitoring):
| Source | Method | Frequency | What Changes |
|---|---|---|---|
| CBIC Customs Notifications | RSS feed from cbic.gov.in | Daily check | Tariff changes, new document requirements, policy amendments |
| DGFT Public Notices | RSS/email subscription | Daily check | Import/export policy changes, HS code restrictions |
| CBP Trade Bulletins | CSMS (Cargo Systems Messaging Service) | Real-time | Entry requirements, system changes |
| FDA Import Alerts | RSS feed from fda.gov | Daily check | New product restrictions, country-specific alerts |
| Canada Gazette | RSS feed | Weekly check | Tariff amendments, regulatory changes |
| CBSA Customs Notices | Email subscription | As published | Documentation requirement changes |
Change Processing Pipeline:
- Detect: Automated monitor picks up new notification/bulletin
- Classify: Nova Micro classifies the change type (tariff, document requirement, restriction, informational)
- Extract: If document-requirement-related, Claude 4.5 extracts the structured rule change
- Diff: Compare extracted change against existing KG rules
- Draft: Generate a proposed KG update (add/modify/remove rules)
- Queue: Place in curator review queue with urgency level
- Review: Human curator approves/rejects within SLA (24h for critical, 72h for standard)
- Apply: Approved changes are applied to KG with version bump
- Notify: Affected users are notified per the existing notification system
Every document rule in the Knowledge Graph carries:
interface DocumentRule {
document_type: string;
// Weight determines how critical this document is
weight: 3.0 | 2.0 | 1.0;
// 3.0 = CRITICAL: Shipment WILL be held at customs without this
// Examples: Bill of Lading, Commercial Invoice, FDA Prior Notice for food
// 2.0 = IMPORTANT: May cause delays, penalties, or additional inspection
// Examples: Certificate of Origin (for preferential rates), Packing List
// 1.0 = STANDARD: Good practice, may be requested during audit
// Examples: Insurance certificate, detailed packing dimensions
// Condition determines WHEN this document is required
is_mandatory: boolean;
condition: string | null;
// null = always required for this HS+corridor combination
// "Claiming preferential tariff rate under USMCA" = conditional
// "Shipment value exceeds $2,500" = value-based condition
// "Wood packaging materials present" = content-based condition
// Category narrows applicability
product_category: string | null;
// null = applies to all products in the HS range
// "pharma" = only for pharmaceutical products
// "food_agriculture" = only for food/ag products
// Provenance for zero-trust compliance
regulatory_body: string;
regulation_reference: string;
source_url: string;
confidence: number; // 0.0-1.0, how confident we are in this rule
reviewed_by: string; // who validated this rule
last_verified_at: datetime; // when was this rule last confirmed current
}Condition Evaluation at Runtime:
When generating a checklist for a shipment, the system evaluates conditions:
def evaluate_condition(rule: DocumentRule, shipment: Shipment) -> bool:
"""Determine if a conditional document is required for this shipment."""
if rule.is_mandatory and rule.condition is None:
return True # Always required
# Parse condition and evaluate against shipment context
condition = rule.condition.lower()
if "preferential tariff" in condition or "trade agreement" in condition:
return shipment.claims_preferential_rate
if "value exceeds" in condition:
threshold = extract_value(condition)
return shipment.total_value > threshold
if "wood packaging" in condition:
return shipment.has_wood_packaging
if "hazardous" in condition or "dangerous goods" in condition:
return any(item.is_hazardous for item in shipment.line_items)
if "cold chain" in condition or "temperature" in condition:
return any(item.requires_cold_chain for item in shipment.line_items)
# For complex conditions, use LLM to evaluate
return llm_evaluate_condition(rule.condition, shipment.to_context())| Task | Effort | Who |
|---|---|---|
| Download & organize source documents (3 countries) | 8 hours | Developer |
| Build LLM extraction pipeline | 16 hours | Developer |
| Run extraction on all source documents | 4 hours | Automated + developer oversight |
| Expert review of extracted rules (~500 rules for 3 countries) | 30-40 hours | Trade compliance expert |
| Build change monitoring pipeline | 16 hours | Developer |
| Set up RSS/notification feeds | 4 hours | Developer |
| Total | ~80 hours dev + 40 hours expert |
This is significantly more robust than scraping because:
- Source documents are saved locally — no dependency on live website availability
- LLM extraction handles varied document formats (PDF, HTML, prose)
- Human review catches errors before they affect compliance decisions
- Change monitoring is event-driven (notifications/RSS), not polling-based scraping
- Every rule has full provenance (source, reviewer, confidence) for audit compliance
// Get document requirements for a shipment based on HS codes + corridor + product category
MATCH (hs:HSCode {code: $hs_code})-[:BELONGS_TO_CATEGORY]->(cat:ProductCategory)
MATCH (cat)-[:REQUIRES_CATEGORY_DOCUMENT {
corridor_origin: $origin,
corridor_destination: $destination
}]->(doc:DocumentType)
OPTIONAL MATCH (cat)-[:REGULATED_BY {country: $destination}]->(reg:RegulatoryBody)
RETURN doc, cat, reg,
cat.risk_level AS risk_level,
cat.special_requirements AS special_requirements
UNION
// Also get corridor-level requirements (not category-specific)
MATCH (hs:HSCode {code: $hs_code})-[:REQUIRES_DOCUMENT {
corridor_origin: $origin,
corridor_destination: $destination
}]->(doc:DocumentType)
RETURN doc, null AS cat, null AS reg, null AS risk_level, null AS special_requirementsProduct categories are assigned risk levels that affect processing:
| Risk Level | Behavior | Example Categories |
|---|---|---|
| Critical | Block shipment until all category docs verified, require manual compliance officer review | Arms & Ammunition (Ch. 93) |
| High | Flag for priority compliance check, require all category-specific documents | Pharmaceuticals (Ch. 30), Chemicals (Ch. 28-29) |
| Medium | Standard compliance check, category documents required but auto-clearable | Food & Agriculture (Ch. 1-24), Textiles (Ch. 50-63) |
| Low | Standard processing, minimal category-specific requirements | General merchandise |
The user's concern is valid: building and maintaining scrapers for every government website across multiple countries is fragile, expensive, and unsustainable. We do NOT rely on web scraping as the primary strategy. Instead, we use a tiered approach:
Tier 1 — Structured Official Downloads (Primary, ~60% of data): These are government-published files in machine-readable formats (CSV, XML, JSON) designed for programmatic consumption. No scraping needed — just download, parse, and ingest.
| Source | Format | How We Get It | Reliability |
|---|---|---|---|
| OFAC SDN List | CSV, XML | Direct download from ofac.treasury.gov — official SLS service | Very high — official US Treasury |
| EU Consolidated Sanctions | XML | Direct download from EC DG Relex — XML with structured tags | Very high — official EU |
| USITC HTS | JSON (13MB), CSV (4MB), XLSX | Direct download from usitc.gov — each revision published separately | Very high — official USITC |
| OpenSanctions | JSON, CSV | Bulk download or API — aggregates 200+ sanctions lists (OFAC, EU, UN) into normalized format | High — best single source for sanctions |
Tier 2 — Official APIs (Secondary, ~10% of data): Some governments provide APIs for real-time or near-real-time data access.
| Source | API Type | How We Get It | Reliability |
|---|---|---|---|
| India ICEGATE | JSON REST API | API client — supports BE/SB filing, exchange rates | Medium-high — government API |
| USITC DataWeb | Query interface | Programmatic query for tariff rates and trade data | High |
| HTS API (htsapi.dev) | REST API | Third-party wrapping official USITC data with duty rates | Medium — third-party |
Tier 3 — Curated Manual Ingestion (Tertiary, ~30% of data): For sources that only publish in PDF/HTML (India CBIC, Canada CBSA, UN sanctions HTML, CBP CROSS rulings), we use a human-in-the-loop curation process rather than brittle scrapers:
- Initial load: Trade compliance experts manually structure the regulations into our Knowledge Graph schema (one-time effort per country)
- Change monitoring: RSS feeds, government gazette subscriptions, and email alerts notify us of changes
- AI-assisted updates: When a change is detected, the RAG pipeline ingests the new document, and an AI agent drafts the Knowledge Graph update for human review
- Expert review: A compliance curator reviews and approves the update before it goes live
This is more sustainable than scraping because:
- Government websites change layout frequently, breaking scrapers
- Regulatory text requires interpretation, not just extraction
- Errors in compliance data have legal consequences — human review is essential
- The volume of changes is manageable (weekly/monthly, not real-time)
Cost of curation: For MVP (3 countries), initial setup is ~2-4 weeks of a trade compliance expert's time. Ongoing maintenance is ~5-10 hours/week across all sources.
| Source | Data | Format | Frequency | URL | Verified |
|---|---|---|---|---|---|
| OFAC SDN List | US sanctioned entities | CSV, XML (official direct download) | Daily | ofac.treasury.gov/ofac-sanctions-lists | ✅ Confirmed — CSV and advanced XML standard |
| EU Consolidated Sanctions | EU sanctioned entities | XML (direct download) | Regular | ec.europa.eu (DG Relex) | ✅ Confirmed — XML with tags, importable to Excel/DBMS |
| UN Sanctions | UN sanctioned entities | Structured HTML (not clean XML) | Regular | scsanctions.un.org/consolidated | |
| USITC HTS | US tariff schedule | JSON (13MB), CSV (4MB), XLSX | As amended | usitc.gov (HTS revisions page) | ✅ Confirmed — official JSON/CSV/XLSX downloads per revision |
| CBP CROSS | US customs rulings database | Searchable web DB | Ongoing | rulings.cbp.gov | |
| Canada Customs Tariff | Canadian tariff schedule | HTML (chapter-by-chapter) | Annual + amendments | cbsa-asfc.gc.ca | |
| India CBIC | Indian customs tariff | As amended | cbic.gov.in | ||
| India ICEGATE | Indian trade facilitation | JSON API (BE/SB filing, exchange rates) | Real-time | icegate.gov.in | ✅ Confirmed — JSON-based API for filings |
| Indian Trade Portal | Trade agreements, regulations | HTML | Ongoing | indiantradeportal.in | |
| OpenSanctions | Aggregated sanctions (200+ lists incl. OFAC, EU, UN) | JSON, CSV (bulk download + API) | Daily | opensanctions.org | ✅ Confirmed — best single source for sanctions aggregation |
| WCO HS Nomenclature | HS code structure (base) | Every 5 years | wcoomd.org |
graph TB
subgraph "Source Adapters (Pluggable)"
A1[OFAC Adapter<br/>CSV/XML Parser]
A2[EU Sanctions Adapter<br/>XML Parser]
A3[UN Sanctions Adapter<br/>XML Parser]
A4[USITC HTS Adapter<br/>JSON/CSV Parser]
A5[CBP CROSS Adapter<br/>Web Scraper]
A6[Canada Tariff Adapter<br/>HTML/PDF Parser]
A7[India CBIC Adapter<br/>PDF/HTML Parser]
A8[ICEGATE Adapter<br/>API Client]
A9[OpenSanctions Adapter<br/>JSON/CSV Parser]
end
subgraph "Ingestion Pipeline"
FETCH[Fetch Raw Data<br/>HTTP/API/Scrape]
PARSE[Parse & Normalize<br/>Source-specific logic]
VALIDATE[Schema Validation<br/>Reject invalid records]
DIFF[Change Detection<br/>Diff against existing KG data]
VERSION[Version Tracking<br/>Assign data version ID]
end
subgraph "Knowledge Graph"
WRITE[Neo4j Writer<br/>Upsert nodes/relationships]
HISTORY[Version History<br/>Preserve old versions]
end
subgraph "Manual Curation"
CURATOR[Curation Interface<br/>Complex regulations]
REVIEW[Expert Review Queue<br/>Ambiguous rules]
end
A1 --> FETCH
A2 --> FETCH
A3 --> FETCH
A4 --> FETCH
A5 --> FETCH
A6 --> FETCH
A7 --> FETCH
A8 --> FETCH
A9 --> FETCH
FETCH --> PARSE
PARSE --> VALIDATE
VALIDATE --> DIFF
DIFF --> VERSION
VERSION --> WRITE
VERSION --> HISTORY
VALIDATE -->|Complex/Ambiguous| CURATOR
CURATOR --> REVIEW
REVIEW --> WRITE
Adapter Pattern:
Each data source has a pluggable adapter implementing a common interface:
# ingestion/adapters/base.py
from abc import ABC, abstractmethod
from typing import List, Optional
from datetime import datetime
class BaseDataAdapter(ABC):
"""Base adapter for regulatory data source ingestion."""
@abstractmethod
async def fetch_raw_data(self) -> bytes:
"""Fetch raw data from the source."""
pass
@abstractmethod
async def parse(self, raw_data: bytes) -> List[dict]:
"""Parse raw data into normalized records."""
pass
@abstractmethod
async def validate(self, records: List[dict]) -> tuple[List[dict], List[dict]]:
"""Validate records against schema. Returns (valid, invalid)."""
pass
@abstractmethod
async def detect_changes(self, new_records: List[dict]) -> dict:
"""Diff new records against existing KG data.
Returns {added: [], modified: [], removed: []}."""
pass
@abstractmethod
def get_source_name(self) -> str:
"""Return the source identifier (e.g., 'ofac_sdn')."""
pass
@abstractmethod
def get_refresh_schedule(self) -> str:
"""Return cron expression for refresh schedule."""
passSchema Validation:
- Every record is validated against a source-specific JSON Schema before KG write
- Invalid records are logged to
data_ingestion_logswith error details - Partial ingestion is allowed — valid records proceed, invalid records are quarantined
Change Detection:
- Each ingestion run diffs new data against existing KG nodes
- Changes are categorized: added, modified, removed
- Only changed records are written to the KG (upsert pattern)
- Every change is versioned with a
data_versionidentifier
Not all regulatory data is structured. Trade rulings, guidance documents, and regulatory interpretations are stored in the RAG store (pgvector) for retrieval-augmented generation:
- CBP CROSS rulings: Full text of customs rulings, chunked and embedded
- Trade agreement text: USMCA, India-Canada CEPA provisions
- Regulatory guidance: FDA import alerts, FSSAI advisories, CBSA D-memoranda
- Document templates: Standard forms and their field requirements
Monitoring Approach:
| Method | Sources | Frequency |
|---|---|---|
| Scheduled polling | OFAC, EU, UN sanctions; USITC HTS; Canada Tariff | Per source schedule (daily/weekly) |
| RSS/Atom feeds | CBP trade bulletins, FDA import alerts | Hourly check |
| API polling | ICEGATE (India) | Real-time where available |
| Manual monitoring | WCO HS updates, trade agreement changes | Quarterly review |
Change Impact Analysis:
When a regulation changes, the platform performs impact analysis:
- Identify affected entities: Query KG for all HS codes, corridors, and document types linked to the changed regulation
- Find active shipments: Query PostgreSQL for all shipments in
document_collectionorunder_reviewstatus that match affected HS codes and corridors - Assess impact severity: Categorize as
informational(minor wording change),action_required(new document needed), orblocking(shipment cannot proceed under new rules) - Notify users: Send notifications to affected shipment owners and custom brokers within 48 hours of detected change
- Offer checklist regeneration: For
action_requiredchanges, prompt users to regenerate their Document Checklist with updated requirements
Scoring is the core value proposition — it tells users exactly what's good, what's bad, and what to fix. There are two distinct scoring systems:
- Document Readiness Score (Phase 1) — Is the document complete and well-formed?
- Compliance Score (Phase 2) — Does the document meet regulatory requirements?
Input: Raw uploaded document (PDF, image, CSV) Model used: Nova Lite (field extraction) + Textract (OCR preprocessing) Output: Structured extraction result with per-field confidence scores
The Document Parser extracts fields based on document type templates:
| Document Type | Required Fields | Conditional Fields |
|---|---|---|
| Commercial Invoice | invoice_number, date, seller_name, buyer_name, item_descriptions, quantities, unit_prices, total_value, currency, incoterms | hs_codes, country_of_origin, payment_terms |
| Bill of Lading | bl_number, shipper, consignee, notify_party, vessel_name, port_of_loading, port_of_discharge, description_of_goods, gross_weight, number_of_packages | container_numbers, seal_numbers, freight_terms |
| Packing List | shipper, consignee, item_descriptions, quantities, net_weight, gross_weight, package_type, number_of_packages | dimensions, marks_and_numbers |
| Certificate of Origin | exporter_name, importer_name, goods_description, hs_code, origin_criteria, origin_country | certifying_authority, certificate_number |
| Purchase Order | po_number, buyer, seller, line_items (description, qty, unit_price), total_value | hs_codes, delivery_terms, payment_terms |
| Customs Declaration | declarant, importer_of_record, entry_type, port_of_entry, hs_codes, declared_value, country_of_origin | bond_number, broker_reference |
| Letter of Credit | lc_number, issuing_bank, beneficiary, applicant, amount, currency, expiry_date | shipment_deadline, documents_required, partial_shipment_allowed |
Per-field output:
{
"field_name": "invoice_number",
"extracted_value": "INV-2026-00451",
"confidence": 0.95,
"bounding_box": {"page": 1, "x": 120, "y": 45, "w": 200, "h": 20},
"extraction_method": "ocr_structured",
"needs_review": false
}The scoring engine is deterministic, not AI-based — it applies rules to the extraction output. This ensures consistency and auditability.
Per-Field Score Calculation:
| Condition | Field Score | Label |
|---|---|---|
| Extracted with confidence >= 0.85 | 1.0 | ✅ Confident |
| Extracted with confidence 0.70-0.84 | 0.7 | |
| Extracted with confidence 0.50-0.69 | 0.3 | |
| Extracted with confidence < 0.50 | 0.0 | ❌ Unreliable |
| Not found / missing | 0.0 | ❌ Missing |
Document-Level Readiness Score Formula:
Document_Readiness_Score = Σ(field_score × field_weight) / Σ(field_weight)
Where field_weight is assigned per document type:
| Weight | Meaning | Example Fields |
|---|---|---|
| 3.0 (Critical) | Document is useless without this | invoice_number, bl_number, total_value, hs_code |
| 2.0 (Important) | Needed for compliance, but document is still identifiable | date, incoterms, country_of_origin, gross_weight |
| 1.0 (Standard) | Useful but not blocking | payment_terms, marks_and_numbers, dimensions |
Document Status Mapping:
| Readiness Score | Status | Visual | Meaning |
|---|---|---|---|
| >= 0.85 | complete |
✅ | All critical fields extracted with high confidence |
| 0.60 - 0.84 | needs_attention |
Some fields missing or low confidence — human review recommended | |
| < 0.60 | non_compliant |
❌ | Critical fields missing or unreadable — document likely needs re-upload |
Cross-Document Validation Rules (bonus checks):
| Rule | Check | Impact |
|---|---|---|
| Invoice-PO match | Invoice total matches PO total (±5%) | Flags discrepancy if mismatch |
| Party consistency | Seller on invoice = shipper on B/L | Flags if names don't fuzzy-match (>80%) |
| HS code consistency | HS codes on invoice match PO line items | Flags mismatches |
| Weight consistency | Gross weight on packing list ≈ B/L weight (±10%) | Flags discrepancy |
| Value consistency | Declared value on customs declaration ≈ invoice total | Flags if >10% difference |
Shipment_Readiness_Score = (count of "complete" documents / total required documents) × 100
Displayed as a percentage on the dashboard. A shipment is "ready" when score = 100%.
Compliance scoring happens in Phase 2 and involves multiple specialized agents working in sequence.
Input: All party names from the shipment (importer, exporter, consignee, notify party) Model used: No LLM — uses fuzzy string matching (Levenshtein distance + Jaro-Winkler) against Neo4j sanctions nodes Output:
{
"party_name": "ABC Trading Co.",
"party_role": "exporter",
"screening_result": "clear", // or "potential_hit"
"matches": [
{
"matched_name": "ABC Trading Company Ltd",
"source_list": "ofac_sdn",
"similarity_score": 0.87,
"entity_id": "OFAC-12345",
"programs": ["IRAN", "SDGT"]
}
]
}Scoring: Binary — clear or potential_hit. Any hit above the threshold (default 85%) blocks the shipment.
Input: Product descriptions from PO line items (where HS code is missing) Model used: Nova Pro (production) / Llama 3.1 8B (local) — needs reasoning for classification Tools: Knowledge Graph query (HS code hierarchy), RAG search (trade rulings, classification guidance) Output:
{
"item_description": "Portable laptop computer, 15 inch, 16GB RAM",
"candidates": [
{"hs_code": "8471.30", "confidence": 0.92, "explanation": "Portable digital automatic data processing machines, weighing not more than 10 kg (HS Chapter 84, Section XVI). Classified under 8471.30 per GRI 1 and Note 5(A) to Chapter 84."},
{"hs_code": "8471.41", "confidence": 0.15, "explanation": "Other data processing machines comprising a CPU and I/O unit. Less likely as item is portable."},
{"hs_code": "8528.52", "confidence": 0.05, "explanation": "Monitors. Unlikely — item is a complete computer, not a display."}
]
}Input: Complete shipment data (documents, extracted fields, HS codes, trade corridor) Model used: Claude Sonnet 4.5 (production) — needs deep reasoning for regulatory interpretation Tools: Knowledge Graph (corridor requirements, regulations), RAG (trade rulings, regulatory guidance) Output: A compliance report with per-check results
Compliance Check Categories:
| Category | What It Checks | Source |
|---|---|---|
| Document Completeness | Are all required documents present per the checklist? | Knowledge Graph (HS + corridor → required docs) |
| Field Validation | Do extracted fields meet format/value requirements? (e.g., valid HS code format, valid country codes, dates not expired) | Deterministic rules |
| Cross-Document Consistency | Do values match across documents? (invoice total vs customs declaration, party names, HS codes) | Deterministic comparison |
| Regulatory Compliance | Does the shipment meet corridor-specific regulations? (e.g., Certificate of Origin required for preferential tariff, phytosanitary cert for food products) | Knowledge Graph + RAG (Claude 4.5 interprets) |
| Product Category Compliance | Does the shipment meet category-specific requirements? (e.g., FDA approval for pharma, FSSAI for food) | Knowledge Graph (product category → regulatory body → requirements) |
| Tariff Accuracy | Are declared HS codes correct? Do tariff rates match? | Knowledge Graph (HS → tariff rates for corridor) |
Per-Check Output:
{
"check_id": "reg_compliance_001",
"check_type": "regulatory_compliance",
"rule_reference": "19 CFR 10.411 — USMCA Certificate of Origin",
"result": "fail",
"severity": "blocking",
"description": "Certificate of Origin is required when claiming preferential tariff rate under USMCA. Document is missing from shipment.",
"source_regulation": "19 CFR Part 10, Subpart P — United States-Mexico-Canada Agreement",
"source_url": "https://www.ecfr.gov/current/title-19/chapter-I/part-10/subpart-P",
"remediation": "Upload a valid USMCA Certificate of Origin (CBP Form 434) signed by the exporter or producer.",
"cross_references": [
{"source": "USMCA Article 5.2", "agrees": true},
{"source": "CBP Ruling HQ H301234", "agrees": true}
]
}Shipment Compliance Score:
| Overall Result | Condition |
|---|---|
pass |
All checks pass |
warning |
All checks pass but some have informational warnings (e.g., tariff rate may change soon) |
fail |
One or more checks fail with blocking severity |
blocked |
Sanctions screening returned a potential_hit that hasn't been resolved |
The compliance report is stored as a structured JSON and can be exported as a PDF with full regulatory citations for audit purposes.
graph TB
subgraph "Phase 1 — Document Readiness"
UPLOAD[Document Upload] --> TEXTRACT[Textract OCR<br/>Nova Lite extraction]
TEXTRACT --> FIELDS[Extracted Fields<br/>+ Confidence Scores]
FIELDS --> RULES[Deterministic Scoring Engine<br/>Weighted field scores]
RULES --> DOC_SCORE[Document Score<br/>complete / needs_attention / non_compliant]
DOC_SCORE --> SHIP_SCORE[Shipment Readiness %<br/>complete docs / total required]
end
subgraph "Phase 2 — Compliance"
SHIP_SCORE --> SANCTIONS[Sanctions Screener<br/>Fuzzy match — no LLM]
SANCTIONS --> HS_CHECK[HS Classifier<br/>Nova Pro — reasoning]
HS_CHECK --> COMPLIANCE[Compliance Checker<br/>Claude 4.5 — deep reasoning]
COMPLIANCE --> REPORT[Compliance Report<br/>pass / warning / fail / blocked]
end