Asset Passport — Backend Architecture Design
Date: February 17, 2026
Status: Approved
Stack: Next.js 16 API Routes + Prisma + PostgreSQL + Socket.io + Custom JWT
Asset Passport is a privacy-first digital identity platform for real-world assets (RWA). The frontend (205 TypeScript files, 19 routes, 97 UI components) is complete. This document defines the backend architecture.
V1 Pain Points (What We're Solving)
Problem
Root Cause
V2 Solution
Rigid schema
Fixed DB columns per field
JSONB for dynamic section data
Tight coupling
Frontend + backend intertwined
Modular services in server/ directory
No multi-tenancy
Single-user assumption
Organization-scoped with org_id on every table
Poor auth/permissions
Simple role check
Custom JWT with granular permissions array
Didn't foresee growth
Hardcoded jurisdictions/types
JSON config files + DB-stored custom schemas
Auth: Custom JWT + middleware (wallet-compatible for future SIWE)
Database: PostgreSQL + JSONB for dynamic section data
Multi-tenancy: Organization-scoped (org_id FK everywhere)
Real-time: Socket.io sidecar for live collaboration
Blockchain: Attestation hashes only (content hash on-chain)
ORM: Prisma with mapped table/column names
2.1 Entity Relationship Diagram
organizations 1──N org_memberships N──1 users
organizations 1──N passports
organizations 1──N assignments
organizations 1──N activity_events
organizations 1──N schema_definitions
users 1──N passports (created_by)
users 1──N passport_sections (attested_by)
users 1──N assignments (verifier_id)
users 1──N assignments (created_by)
users 1──N activity_events (actor_id)
users 1──N attestations (attested_by)
users 1──N refresh_tokens
users 1──N schema_definitions (created_by)
passports 1──N passport_sections (CASCADE)
passports 1──1 privacy_configs (CASCADE)
passports 1──N assignments (CASCADE)
passports 1──N attestations (CASCADE)
Column
Type
Constraints
id
UUID
PK
name
VARCHAR(255)
NOT NULL
slug
VARCHAR(100)
UNIQUE, NOT NULL
settings
JSONB
DEFAULT '{}'
created_at
TIMESTAMPTZ
DEFAULT now()
updated_at
TIMESTAMPTZ
auto
Column
Type
Constraints
id
UUID
PK
email
VARCHAR(255)
UNIQUE, NOT NULL
password_hash
VARCHAR(255)
nullable (wallet-only users)
wallet_address
VARCHAR(42)
UNIQUE, nullable
display_name
VARCHAR(255)
NOT NULL
avatar_url
TEXT
nullable
created_at
TIMESTAMPTZ
DEFAULT now()
updated_at
TIMESTAMPTZ
auto
Column
Type
Constraints
id
UUID
PK
user_id
UUID
FK → users ON DELETE CASCADE
org_id
UUID
FK → organizations ON DELETE CASCADE
role
ENUM(owner,admin,verifier,member)
DEFAULT member
permissions
JSONB
DEFAULT '[]'
invited_by
UUID
FK → users, nullable
created_at
TIMESTAMPTZ
DEFAULT now()
UNIQUE(user_id, org_id)
Column
Type
Constraints
id
UUID
PK
org_id
UUID
FK → organizations ON DELETE CASCADE
name
VARCHAR(255)
NOT NULL
template_id
VARCHAR(100)
NOT NULL
jurisdiction
VARCHAR(100)
NOT NULL
property_type
VARCHAR(100)
NOT NULL
status
ENUM(draft,in_review,active,archived)
DEFAULT draft
created_by
UUID
FK → users
created_at
TIMESTAMPTZ
DEFAULT now()
updated_at
TIMESTAMPTZ
auto
Column
Type
Constraints
id
UUID
PK
passport_id
UUID
FK → passports ON DELETE CASCADE
schema_id
VARCHAR(100)
NOT NULL
state
ENUM(7 states)
DEFAULT empty_required
data
JSONB
DEFAULT '{}'
attested_by
UUID
FK → users, nullable
attested_at
TIMESTAMPTZ
nullable
updated_at
TIMESTAMPTZ
auto
UNIQUE(passport_id, schema_id)
Column
Type
Constraints
id
UUID
PK
passport_id
UUID
FK → passports ON DELETE CASCADE, UNIQUE
access_level
ENUM(public,private)
DEFAULT public
private_fields
JSONB
DEFAULT '{}'
password_hash
VARCHAR(255)
nullable
whitelist
TEXT[]
DEFAULT '{}'
updated_at
TIMESTAMPTZ
auto
Column
Type
Constraints
id
VARCHAR(100)
PK
org_id
UUID
FK → organizations, nullable (null = global)
label
VARCHAR(255)
NOT NULL
description
TEXT
nullable
required
BOOLEAN
DEFAULT false
fields
JSONB
DEFAULT '[]'
ui_hints
JSONB
DEFAULT '{}'
created_by
UUID
FK → users, nullable
created_at
TIMESTAMPTZ
DEFAULT now()
updated_at
TIMESTAMPTZ
auto
Column
Type
Constraints
id
UUID
PK
org_id
UUID
FK → organizations ON DELETE CASCADE
passport_id
UUID
FK → passports ON DELETE CASCADE
verifier_id
UUID
FK → users
section_ids
TEXT[]
NOT NULL
status
ENUM(pending,in_progress,completed)
DEFAULT pending
created_by
UUID
FK → users
created_at
TIMESTAMPTZ
DEFAULT now()
completed_at
TIMESTAMPTZ
nullable
Column
Type
Constraints
id
UUID
PK
org_id
UUID
FK → organizations ON DELETE CASCADE
type
VARCHAR(50)
NOT NULL
actor_id
UUID
FK → users
actor_name
VARCHAR(255)
NOT NULL (denormalized)
actor_role
VARCHAR(50)
NOT NULL
target_id
UUID
NOT NULL
target_type
VARCHAR(50)
NOT NULL
message
TEXT
NOT NULL
metadata
JSONB
DEFAULT '{}'
created_at
TIMESTAMPTZ
DEFAULT now()
Column
Type
Constraints
id
UUID
PK
passport_id
UUID
FK → passports ON DELETE CASCADE
content_hash
VARCHAR(66)
NOT NULL
tx_hash
VARCHAR(66)
nullable
chain_id
INTEGER
nullable
block_number
BIGINT
nullable
attested_by
UUID
FK → users
created_at
TIMESTAMPTZ
DEFAULT now()
Column
Type
Constraints
id
UUID
PK
user_id
UUID
FK → users ON DELETE CASCADE
token_hash
VARCHAR(255)
NOT NULL
expires_at
TIMESTAMPTZ
NOT NULL
created_at
TIMESTAMPTZ
DEFAULT now()
Table
Index
Purpose
org_memberships
UNIQUE(user_id, org_id)
One membership per org
org_memberships
INDEX(org_id)
List org members
passports
INDEX(org_id, status)
Filter by status
passports
INDEX(org_id, jurisdiction)
Filter by jurisdiction
passports
INDEX(created_by)
User's passports
passport_sections
UNIQUE(passport_id, schema_id)
One section per schema
passport_sections
INDEX(passport_id)
Sections for passport
passport_sections
GIN(data)
Query JSONB fields
privacy_configs
UNIQUE(passport_id)
One config per passport
schema_definitions
INDEX(org_id)
Org's custom schemas
assignments
INDEX(org_id, status)
Filter by status
assignments
INDEX(verifier_id)
Verifier's queue
assignments
INDEX(passport_id)
Passport's assignments
activity_events
INDEX(org_id, created_at DESC)
Activity feed
activity_events
INDEX(target_id, target_type)
Entity events
attestations
INDEX(passport_id)
Attestation history
attestations
INDEX(content_hash)
Hash verification
refresh_tokens
INDEX(token_hash)
Token lookup
refresh_tokens
INDEX(user_id)
Revoke user tokens
2.4 JSONB Column Structures
passport_sections.data:
{ "street" : " 123 Sheikh Zayed Road" , "city" : " Dubai" , "country" : " UAE" , "latitude" : 25.0772 }
privacy_configs.private_fields:
{ "financial" : [" revenue" , " netIncome" ], "ownership" : [" ownerNationalId" ] }
org_memberships.permissions:
[" passport:read" , " passport:write" , " section:verify" , " assignment:create" ]
organizations.settings:
{ "defaultJurisdiction" : " uae" , "branding" : { "primaryColor" : " #FF4F91" }, "features" : { "attestation" : true } }
/api/auth/register POST Create account
/api/auth/login POST Login → tokens
/api/auth/refresh POST Rotate refresh token
/api/auth/logout POST Revoke refresh token
/api/auth/wallet POST SIWE wallet auth
/api/auth/me GET Current user + orgs
/api/organizations GET|POST
/api/organizations/[orgId] GET|PATCH|DELETE
/api/organizations/[orgId]/members GET|POST
/api/organizations/[orgId]/members/[userId] PATCH|DELETE
/api/organizations/[orgId]/activity GET (paginated)
/api/passports GET|POST
/api/passports/[id] GET|PATCH|DELETE
/api/passports/[id]/sections GET
/api/passports/[id]/sections/[schemaId] GET|PUT
/api/passports/[id]/privacy GET|PUT
/api/passports/[id]/attest POST
/api/passports/[id]/export GET
/api/assignments GET|POST
/api/assignments/[id] GET|PATCH
/api/assignments/[id]/review POST
/api/schemas GET
/api/schemas/custom POST
/api/schemas/custom/[id] PATCH|DELETE
/api/templates GET
/api/templates/custom POST
/api/templates/custom/[id] PATCH|DELETE
/api/jurisdictions GET
Access token: 15 min expiry, contains user ID + org memberships + roles.
Refresh token: 7 day expiry, stored hashed in DB, rotated on use.
JWT claims:
{
"sub" : " user-uuid" ,
"email" : " user@example.com" ,
"orgs" : [{ "id" : " org-uuid" , "role" : " owner" , "permissions" : [" passport:*" ] }],
"wallet" : " 0x..." ,
"iat" : 1739750400 ,
"exp" : 1739751300
}
Parse + verify JWT
Resolve org from X-Org-Id header
Check role + permissions for route
Inject context: { userId, email, orgId, role, permissions }
Role
Passports
Sections
Assignments
Schemas
Org
owner
CRUD all
R/W all
Create, view all
Create custom
Full
admin
CRUD all
R/W all
Create, assign
Create custom
Full
verifier
Read assigned
Read all, write assigned
View own
Read only
No
member
Read all, create own
R/W own
View own
Read only
No
4. Real-Time Architecture
Socket.io server running alongside Next.js (port 3001). Shares JWT verification. Rooms keyed by passport ID.
Event
Direction
Payload
passport:join
Client → Server
{ passportId }
passport:leave
Client → Server
{ passportId }
section:updated
Server → Room
{ passportId, schemaId, data, updatedBy }
section:state-changed
Server → Room
{ passportId, schemaId, newState }
presence:update
Server → Room
{ users: [{ id, name, avatar, activeSection }] }
assignment:created
Server → User
{ assignment }
passport:status-changed
Server → Room
{ passportId, newStatus }
API route mutates data → publishes to Redis/PG NOTIFY → WS sidecar broadcasts to room.
src/
├── app/api/ # API routes (thin handlers)
├── server/
│ ├── db/prisma/ # Schema + migrations
│ ├── middleware/ # auth, org-scope, permissions
│ ├── services/ # Business logic per domain
│ ├── validation/ # Schema + jurisdiction validation
│ └── ws/ # Socket.io sidecar
├── lib/ # Shared types + utilities
└── core/ # Schema registry, template engine
Phase
Changes
Frontend Impact
1. DB + Auth
Prisma schema, auth routes
Login/register pages, JWT storage
2. API Routes
Passport/section CRUD
Replace localStorage with API calls
3. Zustand as Cache
Client-side cache backed by API
Add React Query for server state
4. Real-time
WS sidecar, presence
Socket.io client, live updates
5. Attestation
Content hashing + on-chain
"Attest" button on passport detail
Layer
Technology
Why
Framework
Next.js 16
Already in use, API Routes for backend
ORM
Prisma
Type-safe, migrations, JSONB support
Database
PostgreSQL
JSONB for dynamic data, relational for structure
Auth
Custom JWT (jsonwebtoken)
Full control, wallet-compatible
Real-time
Socket.io
Rooms, presence, reconnection built-in
Validation
Zod
Already in use, schema-based validation
Caching
React Query (TanStack)
Server state management for frontend
Blockchain
ethers.js
Content hash attestation on Integralayer