Skip to content

Instantly share code, notes, and snippets.

@Aboudjem
Created February 17, 2026 08:22
Show Gist options
  • Select an option

  • Save Aboudjem/d77cf5bc149d10301e1a62ab37909abb to your computer and use it in GitHub Desktop.

Select an option

Save Aboudjem/d77cf5bc149d10301e1a62ab37909abb to your computer and use it in GitHub Desktop.
Asset Passport — Backend Architecture Design (Next.js + Prisma + PostgreSQL + Socket.io + Custom JWT)

Asset Passport — Backend Architecture Design

Date: February 17, 2026 Status: Approved Stack: Next.js 16 API Routes + Prisma + PostgreSQL + Socket.io + Custom JWT


1. Context & Goals

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

Key Decisions

  • 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. Database Schema

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)

2.2 Tables

organizations

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

users

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

org_memberships

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)

passports

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

passport_sections

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)

privacy_configs

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

schema_definitions

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

assignments

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

activity_events

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()

attestations

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()

refresh_tokens

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()

2.3 Indexes

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 } }

3. API Layer

3.1 Route Structure

/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

3.2 Auth Flow

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
}

3.3 Middleware Chain

  1. Parse + verify JWT
  2. Resolve org from X-Org-Id header
  3. Check role + permissions for route
  4. Inject context: { userId, email, orgId, role, permissions }

3.4 Permission Model

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

Sidecar WebSocket Server

Socket.io server running alongside Next.js (port 3001). Shares JWT verification. Rooms keyed by passport ID.

Events

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 }

Flow

API route mutates data → publishes to Redis/PG NOTIFY → WS sidecar broadcasts to room.


5. Project Structure

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

6. Migration Strategy

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

7. Technology Stack

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment