Overview & Architecture

EmergeFi is an institutional-grade RWA tokenization platform. Investors deposit stablecoins into pools backed by real-world assets (invoice financing, earned wage access, hotel STOs) and earn yield through LP tokens.

Tech Stack

🖥️ Frontend
Vite + React + TypeScript
React Router · Tailwind 4 · RainbowKit · shadcn/ui
⚡ Backend
AWS CDK + Lambda (Node.js)
API Gateway v2 · CloudFront · Supabase JS
📜 Smart Contracts
Foundry + Solidity
OpenZeppelin · 6 contracts · Base Chain
🗄️ Database
Supabase (PostgreSQL + RLS)
Hosted · Row-level security · supabase-js
🔗 Chain
Base Sepolia + Base Mainnet
+ KAIA (prod) · SIWE auth · wagmi + viem
👤 Auth
SIWE (Sign-In with Ethereum)
JWT via jose · RainbowKit · 30-min timeout

Chain Deployment

🧪 Dev
Base Sepolia
Testnet for all development and staging
🌐 Prod
Base Mainnet + KAIA
Dual-chain production deployment

Smart Contract Registry

Contract Standard Purpose
EmergeFiLPToken ERC-20 + AccessControl LP token — whitelist-only transfers, auto-yield settlement
EmergeFiEscrow AccessControl + ReentrancyGuard USDC escrow — 7-day auto-refund protection
EmergeFiEscrowFactory Factory pattern Deploys new escrow instances per pool
EmergeFiReceipt ERC-721 Proof of deposit, status tracking (PENDING → LP_VERIFIED / REFUNDED)
EmergeFiKYCSoulbound Non-transferable ERC-721 On-chain KYC status via SumSub — non-transferable identity credential
EmergeFiPool AccessControl Pool management — deposits, NAV, role-based access

Core Concepts

Foundational models and mechanisms that define how EmergeFi works. Cards with 🟡 BD or 🟠 PD badges have open decisions — click the badge to view options.

🔀 Dual LP Issuance Model
Two modes: Model A (Fund-Issued) — the fund mints its own LP token; EmergeFi verifies receipt via RPC. Model B (EmergeFi-Issued) — EmergeFi deploys and mints the LP token on the fund's behalf. Same investor UX either way.
Model A: Fund mints on its own contract → Operator verifies LP via RPC → USDC released from escrow. Model B: EmergeFi deploys LP contract on fund’s behalf → mints LP to investor → USDC released simultaneously.
💰 NAV Token Pricing
LP token price floats between $0.00–$1.00 (no floor — PD6 removed Feb 19). Token price IS the loss indicator — no separate PF multiplier. New investors pay the current NAV (fair entry). Investment always stays open — never auto-blocked. tokens_minted = deposit / nav_per_token.
Deposit $10,000 when NAV = $0.92 → you receive 10,000 / 0.92 = 10,869 tokens. At writedown to $0.92 your tokens are worth $10,000 but you get more tokens than original investors. On recovery to $1.00 your tokens are worth $10,869.
🔮 Oracle-Driven NAV Updates
Fund operator calculates NAV off-chain → reports to oracle endpoint → oracle validates and posts updateNAV() on EmergeFiPool → NAV decreases enter 24h timelock, increases apply immediately. No safety bounds (PD6 removed). timelock. NAV increases: immediate.
Operator submits NAV off-chain → oracle validates and calls updateNAV() on EmergeFiPool → if decrease: enters 24h pending queue → after timelock: nav_per_token updated on-chain. NAV increases skip the queue and apply immediately.
🛡️ Reserve Fund
Proposed: 10% of pool deposits allocated as first-loss reserve. Covers losses before NAV drops below $1.00. Open questions: per-pool vs platform-wide %, how reserve is funded (upfront vs accrued), what triggers payout vs writedown.
Fund operator sets a reserve % at pool creation → reserve absorbs loss first → NAV only drops once reserve is exhausted. Proposed: 10% of pool deposits allocated to reserve at creation time (not accrued over time). Reserve payout vs writedown trigger is an open question (BD4).
📈 Yield Model
Recommended: Distributing — LP tokens at $1.00 fixed, yield paid separately as USDC or LP tokens. Yield accrues on nominal balance (original deposit), not NAV-adjusted value. Auto-settled before any LP transfer.
Admin records a yield distribution event → LP price stays fixed at $1.00 → USDC (or LP tokens) distributed separately to investor wallets. Yield formula: nominal_amount × (APY / 365) × days_held. NAV fluctuation does not affect yield calculation.
🔒 Escrow Architecture
USDC goes to an EmergeFi escrow contract (on the fund's chain), not directly to the fund. Operator releases escrowed USDC only after LP issuance confirmed. 7-day auto-refund if LP not issued — no manual intervention needed.
Investor deposits USDC → EmergeFiEscrow holds funds → LP issued to investor → Operator calls releaseToFund() → USDC transferred to fund treasury. If LP not issued within 7 days: autoRefund() executes, USDC returned to investor.
🎫 Receipt Token (ERC-721)
Proof-of-deposit NFT minted when investor deposits to escrow. Tracks status: MINTED → LP_PENDING → LP_VERIFIED or → REFUNDED. Not the LP token — just a receipt. Burned when investment fully settles.
🪪 KYC / Soulbound Token
SumSub integration for KYC/KYB. On approval, a non-transferable ERC-721 (Soulbound Token) is minted on-chain. kyc_status = APPROVED required before any investment. ❓ Open: what happens if KYC is revoked — can investor still redeem?

Investment Lifecycle

End-to-end flow from investor deposit to yield distribution across both LP issuance models.

Investor
System (Auto)
Operator
Admin
Fund Issuer

Phase 1 — Deposit (Both Models)

① Investor connects wallet & passes KYC Investor
SumSub KYC → SBTSoulbound Token — a non-transferable on-chain credential that proves identity verification minted on-chain → kyc_status = APPROVED
② Investor selects pool & enters stablecoin amount Investor
System checks: is_paused = false, kyc_status = APPROVED. NAV model keeps investment open during writedowns — no auto-block.
③ Stablecoin transferred to escrow contract System
On fund's chain → escrow_tx_hash recorded in investments table
Receipt TokenA proof-of-deposit NFT issued to the investor while their investment is being processed and LP tokens haven't arrived yet (ERC-721) minted to investor System
receipt_token_id stored → status = PENDING
⑤ Refund timer starts System
refund_eligible_at = deposit time + 7 days. If LP not issued by then, auto-refund triggers.
⑥ NAV snapshot stored System
tokens_minted = stablecoin_amount / nav_per_token — LP tokens issued at fair current NAV price. nav_at_investment snapshot stored. Model A/B LP issuance follows (steps ⑦–⑩).

Model A Fund-Issued LP

The fund mints and manages its own LP token on its own smart contract. EmergeFi does not control the LP — it only verifies that the investor received LP by reading the fund's contract via RPC.

Joob, Credix, Maple, Centrifuge — any fund with its own LP token contract

⑥ Fund mints LP tokens to investor wallet Fund
Fund handles LP issuance on their own contract, outside of EmergeFi. EmergeFi has no control over this step.
⑦ Operator verifies LP arrived in investor wallet Operator
EmergeFi reads the fund's LP contract via RPC to confirm the investor's balance → lp_verified_at set
⑧ Release escrowed USDC to fund treasury Operator
Only after LP is verified → USDC released from escrow to fund_treasury_addressusdc_release_tx_hash recorded
⑨ Receipt Token → LP_VERIFIED System
Investment fully settled. Investor now holds fund's LP token + receipt token as proof.

Model B EmergeFi-Issued LP

For partners that don't have their own LP token infrastructure. EmergeFi deploys and manages the LP contract on the partner's behalf.

Traditional SPVs, new partners without on-chain presence

⑥ Release escrowed USDC to fund treasury Operator
USDC sent from escrow to fund_treasury_address
⑦ EmergeFi mints LP tokens to investor Operator
Minted on EmergeFi-deployed contract → lp_mint_tx_hash recorded → lp_token_events log (MINT)
⑧ Receipt Token → LP_VERIFIED System

Phase 2 — Yield Distribution

💰 DISTRIBUTING Mechanism
LP tokens priced at $1 fixed. Yield distributed as additional USDC or LP tokens — not through NAV appreciation. Clear, predictable returns for institutional investors.
📐 Yield Calculation
Yield accrues on nominal balanceThe original investment amount before any NAV-adjusted token value is applied (not NAV-adjusted). Auto-settled before any LP transfer. Admin records distributions in database.

Phase 3 — Redemption

🔄 Two-Step Admin Approval Queue
Investor submits a redemption request. NAV is snapshot-locked at request time and never recalculated — protecting against further drops. Requests processed FIFO.
PENDING RECOMMENDED APPROVED PROCESSING COMPLETED

Operator reviews payout + pool liquidity → recommends. Admin gives final approval → USDC payout sent → payout_tx_hash logged.

Writedown & NAV

How NAV reflects real-world asset value changes. Covers writedown mechanics, oracle update flow, timelock enforcement, and portfolio display guidelines. For redemption payout calculations, see the Redemption section.

Dual Loss Protection

🛡 Layer 1: Reserve Fund
reserve_percentage (default 10%) of each pool's deposits. Covers first-loss events before touching investor principal. Configurable per pool.
📈 Layer 2: NAV Token PriceNet Asset Value per token — floats between $0.00 and $1.00 (no floor; PD6 removed). Capped at $1.00 max. New investors always buy at the current fair price.
When losses exceed reserve: nav_per_token drops below $1.00.
nav_per_token = $0.92 → 8% writedown reflected in token price.

Consequences when NAV < $1.00:
• New investments stay open — new investors buy at the lower fair price, getting more tokens
• Existing yield continues on nominal deposit amount
• Redemptions pay tokens × nav_at_request + yield
• NAV decreases require 24-hour timelockA forced waiting period before a NAV decrease takes effect. Increases apply immediately.

NAV Update Timeline

Oracle posts NAV update Oracle
Fund operator reports NAV off-chain → EmergeFi oracle validates → updateNAV() called on EmergeFiPool. Row created in nav_history with status PENDING.
24-hour timelock (decreases only) System
effective_at = now() + 24h. Notification sent to operators + fund issuers. NAV increases apply immediately — no timelock.
NAV takes effect System
pools.nav_per_token updated, nav_history row marked APPLIED. Investment stays open — new investors pay the updated fair price.
Offline: Last NAV maintained until oracle resumes Admin
PD4 decided: No fallback. NAV stays at last known value. Admin can manually pause the pool for serious cases via the admin panel. source = oracle for normal updates.

How Admin Calculates a NAV Writedown

When a loss event occurs (e.g. a borrower defaults), the Oracle posts a new nav_per_token value on-chain via updateNAV(). Here's the step-by-step logic:

📖 Step-by-Step Example
Scenario: Joob EWA Fund pool has $1,000,000 in total deposits. A borrower defaults on a $120,000 loan.

Step 1 — Check the reserve fund first
The pool has a 10% reserve = $1,000,000 × 0.10 = $100,000
The reserve covers $100,000 of the $120,000 loss. Remaining uncovered loss = $20,000

Step 2 — Calculate the new NAV
The uncovered loss reduces nav_per_token. The formula is:
New NAV = Current NAV × (1 − Uncovered Loss ÷ Total Deposits)
New NAV = $1.00 × (1 − $20,000 ÷ $1,000,000) = $1.00 × 0.98 = $0.98

Step 3 — Apply the 24-hour timelock
NAV decreases require a 24-hour waiting period before taking effect. NAV increases (recovery) apply immediately.
✓ Decrease is 2% → submitted as a pending NAV update
✓ 24-hour timelock applies — stakeholders can review or prepare
✓ NAV capped at $1.00 max — cannot increase above initial value

Step 4 — What this means for existing investors
An investor who deposited $10,000 now has:
Tokens still = 10,000 (from original deposit at $1.00 NAV)
Token value = 10,000 × $0.98 = $9,800
They've lost $200 of token value (2%), but their yield still accrues on the full $10,000.

Step 5 — New investor gets fair entry
A new investor depositing $10,000 at $0.98 NAV gets:
tokens_minted = $10,000 ÷ $0.98 = 10,204 tokens → proportionally more tokens than original investors at $1.00. Fair entry.
🔢 Quick Reference Formula
1. Uncovered Loss = Total Loss − Reserve Fund
2. NAV Decrease % = Uncovered Loss ÷ Total Pool Deposits
3. New NAV = Current NAV × (1 − NAV Decrease %)
4. Existing investor value = tokens_minted × New NAV
5. New investor tokens = deposit_amount ÷ New NAV

If Current NAV is already below $1.00 (a previous writedown happened), use the current NAV in step 3. Writedowns are cumulative.

Portfolio NAV Display — Design Spec DECIDED

When a pool's NAV is below $1.00, the portfolio page must show the writedown transparently but without alarming design patterns. Investors are sophisticated — hiding information erodes trust faster than showing it.
✓ DO
Show: "Invested: $1,000 → Current Value: $880 (NAV: $0.88)"
Use neutral colors (grey/blue info card)
Link to NAV history timeline for that pool
Show NAV governance bounds as reassurance
✗ DON'T
Red warning banners or alarm iconography
Words like "loss", "default", "danger"
Hide the writedown behind a click/expand
Show NAV change without context or governance info

→ See Redemption for payout calculations and NAV scenario examples.

Redemption

How investors exit positions. Payout is locked at NAV-at-request time. Two-step admin approval queue — Operator recommends, Admin approves. For how NAV changes are calculated and applied, see Writedown & NAV.

Payout Formula

Token valuetokens_minted × nav_at_request
Accrued yieldnominal × (APY / 365) × days_held
Total payouttoken_value + accrued_yield
Example: Joob EWA Fund, NAV $0.92, 10,000 tokens, 15% APY, 45 days
Total: $9,384.93

Redemption by NAV Scenario

Category A: Standard Entry (invested at $1.00)

A1 — Normal Redemption
NAV at investment: $1.00 · NAV at redemption: $1.00
Deposit $10,000 at NAV $1.00 → 10,000 tokens
Token Value10,000 × $1.00 = $10,000.00
Accrued Yield$10,000 × (15%/365) × 90 days = $369.86
Total Payout$10,369.86
Full principal returned + yield. Simplest case.
⚠️ A2 — Redemption During Writedown
NAV at investment: $1.00 · NAV at redemption: $0.85
Deposit $10,000 at NAV $1.00 → 10,000 tokens
Token Value10,000 × $0.85 = $8,500.00
Accrued Yield$10,000 × (15%/365) × 90 days = $369.86
Total Payout$8,869.86
Investor absorbs 15% NAV loss on token value.
Yield still calculated on nominal $10,000 (not NAV-adjusted).
NAV is locked at request time — if NAV drops further after request, investor keeps $0.85.

Category B: Discounted Entry (invested below $1.00 — "Fair Entry")

📈 B1 — Invested Low, NAV Recovered
NAV at investment: $0.80 · NAV at redemption: $0.95
Deposit $10,000 at NAV $0.80 → 12,500 tokens
Token Value12,500 × $0.95 = $11,875.00
Accrued Yield$10,000 × (15%/365) × 90 days = $369.86
Total Payout$12,244.86
Investor gained from NAV recovery ($0.80 → $0.95).
Got 12,500 tokens instead of 10,000 — the "fair entry" principle with upside.
➡️ B2 — NAV Unchanged, Yield Only
NAV at investment: $0.85 · NAV at redemption: $0.85
Deposit $10,000 at NAV $0.85 → 11,765 tokens
Token Value11,765 × $0.85 = $10,000.25
Accrued Yield$10,000 × (15%/365) × 90 days = $369.86
Total Payout$10,370.11
No NAV movement — investor gets principal back + yield.
Higher token count but token value × NAV = original deposit. Pure yield play.
🔻 B3 — Invested Low, NAV Dropped Further
NAV at investment: $0.85 · NAV at redemption: $0.70
Deposit $10,000 at NAV $0.85 → 11,765 tokens
Token Value11,765 × $0.70 = $8,235.50
Accrued Yield$10,000 × (15%/365) × 90 days = $369.86
Total Payout$8,605.36
Discounted entry does NOT guarantee safety.
NAV dropped further $0.85 → $0.70. Investor loses ~14% on token value.
Entering during a writedown carries risk if the underlying asset continues to deteriorate.
⚡ Early Redemption Penalty (applies to all cases)
If the investor redeems BEFORE the pool's lock-up period ends, an early exit penalty is applied to the accrued yield.
penalty_amount = accrued_yield × early_redemption_penalty_pct
Example applied to Case A2:
Accrued yield: $369.86 · Pool penalty: 50% of accrued yield
Penalty amount: $369.86 × 0.50 = $184.93
Adjusted yield: $369.86 − $184.93 = $184.93

Token value: $8,500.00 (unchanged — penalty only affects yield)
Total payout: $8,500.00 + $184.93 = $8,684.93

Note: Penalty applies to yield only, never to token value / principal.
This is the worst-case scenario: NAV loss + early penalty combined.
BD2 (penalty policy) and BD6 (lock-up structure) are still open decisions.
In all 5 scenarios: Yield always accrues on the nominal deposit amount ($10,000), never on the NAV-adjusted token value. NAV is always locked at the moment of redemption request and cannot be recalculated after submission.

Payout Example

Joob EWA Fund · NAV $0.92 · 10,000 tokens held · 15% APY · 45 days
Token Value 10,000 tokens × $0.92 = $9,200.00
Accrued Yield $10,000 × (0.15÷365) × 45 = $184.93
Total Payout $9,384.93
total = (tokens × nav_at_request) + (nominal × APY/365 × days)

Redemption Flow (Two-Step Admin Queue)

Investor
Operator
Admin
System
Investor requests redemption Investor
Sees confirmation modal: token count, NAV price, token value, accrued yield, total payout + warning: "You will NOT benefit if the NAV recovers after your request is submitted."
NAV snapshot locked → status = PENDING System
nav_at_request captured from current pools.nav_per_token. Payout = tokens_minted × nav_at_request + yield. One active request per investment (no duplicates). FIFOFirst In, First Out — requests are processed in the order they were received; earliest first queue.
Operator reviews → "Recommend Approval" Operator
Verifies payout amount + checks pool liquidity → status = RECOMMENDED. recommended_by recorded.
Admin reviews → "Approve & Process" Admin
Final check → status = APPROVED → PROCESSING. USDC sent from pool → status = COMPLETED. payout_tx_hash logged.
OR: Admin rejects → REJECTED
Rejection reason sent to investor. Investor may request again.

Key Rules

📸 NAV Snapshot
nav_at_request is locked at request time from pools.nav_per_token. If NAV drops further after request, investor keeps the higher NAV. If NAV recovers, payout stays at the lower request-time NAV. Payout = tokens × nav_at_request + yield.
📊 Yield Basis
Accrued yield calculated on nominal balance (not NAV-adjusted token value). Full yield paid to incentivize patience even in writedown pools.
🔒 Early Redemption
If redemption during lockup period → per-pool penalty applies (e.g., 50% of accrued yield forfeited). Configurable per pool.
📋 FIFO Processing
Oldest requests processed first. Admin checks pool liquidity before approval. Insufficient liquidity → request stays in queue (not rejected).

Smart Contracts

Deployed contracts, their roles, and on-chain access control. Oracle architecture is an open product decision.

Deployed Contracts

Contract Standard Purpose
EmergeFiLPToken ERC-20 + AccessControl LP token — whitelist-only transfers, auto-yield settlement before any transfer
EmergeFiEscrow AccessControl + ReentrancyGuard USDC escrow with 7-day auto-refund, reentrancy protection
EmergeFiEscrowFactory Factory Deploys new escrow instances per pool — keeps escrow funds isolated per pool
EmergeFiReceipt ERC-721 Proof of deposit NFT with status tracking. Burned on full settlement
EmergeFiKYCSoulbound Non-transferable ERC-721 On-chain KYC status via SumSub — non-transferable identity credential
EmergeFiPool AccessControl Pool management — deposits, NAV, role-based admin actions
updateNAV() (integrated into EmergeFiPool — PD1 decided) ✅ PD1 updateNAV() + ORACLE_ROLE added directly to EmergeFiPool.sol. No separate contract needed.

Contract Roles

Role Capabilities
DEFAULT_ADMIN_ROLE Grant/revoke all roles, emergency functions, pause contracts
MINTER_ROLE Mint LP tokens (Model B), mint Receipt Tokens
ESCROW_MANAGER_ROLE Release USDC from escrow, process auto-refunds
VERIFIER_ROLE Update LP verification status (Model A)
ORACLE_ROLE Post NAV updates via updateNAV() on EmergeFiPool (PD1/PD2 — decided)

RBACRole-Based Access Control — defines who can do what based on their assigned role & Permissions

Three-tier role hierarchy across platform admin and fund-scoped access.

👑 Admin
Full platform control

Panel: /admin
Scope: All pools, all funds

Exclusive powers:
• Create/delete pools & issuers
• Approve redemptions (final)
• Monitor NAV / Pool Pause
• Pause/unpause pools
• Manage roles (add/remove operators & fund managers)
• Configure notifications & settings
⚙️ Operator
Day-to-day operations

Panel: /admin (limited)
Scope: All pools (restricted actions)

Can do:
• Process investments (verify LP, release USDC, mint LP)
• Record yield distributions
• Edit pool & issuer details
Propose redemptions (not approve)
• View audit log, export CSV
📁 Fund Manager
Read-only, fund-scoped

Panel: /fund-admin
Scope: Own fund(s) only

Can do:
• View fund dashboard & metrics
• View investor list & history
• View redemption requests (own fund)
• View yield records & LP ledger
• Export CSV (own fund data)

Full Permission Matrix

Action 👑 Admin ⚙️ Operator 📁 Fund Mgr
Platform Administration
View admin dashboard
View audit log
Configure notifications
Add/remove Operators
Add/remove Fund Managers
Export CSV (all data)
Pool Management
Create new pool
Edit pool details
Set risk rating (1-5)
Pause / unpause pool
Archive / delete pool
View NAV / Pool Pause
View NAV / Oracle Status ✓ own
Issuer Management
Create new issuer
Edit issuer details
Set issuer status
Add/remove fund manager wallets
Investment Processing
View investment queue
Verify LP (Model A)
Release USDC from escrow
Mint LP tokens (Model B)
Redemption Processing (Two-Step)
View redemption queue Own fund
Propose / recommend redemption
Approve redemption (final)
Process USDC payout
Reject redemption
Fund Admin Panel
View fund dashboard Own fund
View investor list Own fund
Export fund CSV Own fund

Auth & Route Protection

🔐 Authentication
Same wallet-based auth as investors (SIWESign-In With Ethereum — an authentication standard that lets users log in by signing a message with their crypto wallet). Role check on route load via GET /api/auth/role. 30-minute session timeout. No multi-sig for now (single signer + role separation + audit log).

Session response includes wallet_address. OIDC provider cache uses LRU strategy (SHA-678/679 in Linear).

🗄 Database Tables
platform_admins — Admin + Operator roles (wallet, role, is_active)

issuer_admins — Fund Manager roles (wallet, role, issuer_id scope, is_active)

Status State Machines

All status fields and their valid transitions across the system.

Receipt Token Status investments.receipt_status
PENDING MATCHED LP_VERIFIED
PENDING REFUNDED (7-day timeout, no LP issued)

PENDING: USDC in escrow, waiting for LP issuance.
MATCHED: LP confirmed (Model A verified, Model B minted).
LP_VERIFIED: Receipt fully settled, USDC released to fund.
REFUNDED: LP not issued within 7 days, USDC returned.

KYC Status users.kyc_status
PENDING IN_PROGRESS APPROVED
IN_PROGRESS REJECTED

PENDING: User registered, hasn't started KYC.
IN_PROGRESS: SumSub verification underway.
APPROVED: KYC passed, SBT minted on-chain.
REJECTED: Failed verification. User can retry.

Redemption Request Status redemption_requests.status
PENDING RECOMMENDED APPROVED PROCESSING COMPLETED
PENDING or RECOMMENDED REJECTED (+ reason sent to investor)

PENDING: Investor submitted request. NAV snapshot (nav_at_request) locked at current price.
RECOMMENDED: Operator reviewed and recommended approval.
APPROVED: Admin gave final approval.
PROCESSING: USDC transfer initiated.
COMPLETED: USDC received by investor. payout_tx_hash recorded.
REJECTED: Admin denied with reason. Investor may re-request.

Pool Status Flags pools table

Two flags control pool behavior — investment stays open during NAV changes:

is_paused Manual admin toggle. Paused pools hidden from dashboard (unless user has position). No new deposits allowed. The only way to block investment — NAV changes never auto-block.
nav_per_token Decimal (default $1.00, capped at $1.00 max, no floor). Updated by Oracle via updateNAV() with 24h timelock for decreases; increases apply immediately. Drives token minting and redemption payout calculations.
Issuer Status issuers.status
ACTIVE PAUSED OFFBOARDED

ACTIVE: Issuer operational, pools accepting investments.
PAUSED: Temporarily suspended. Existing positions remain. Reversible.
OFFBOARDED: Permanently removed. All pools should be fully redeemed first.

Why issuer-level pause? If an entire fund issuer (e.g., Joob) needs to be suspended for compliance reasons, contract renegotiation, or regulatory hold, pausing at the issuer level cascades to all their pools — rather than pausing each pool individually.

NAV Update Status nav_history.status
PENDING APPLIED

PENDING: NAV decrease submitted. 24h timelock active. Admin can cancel during this window.
APPLIED: Timelock expired, new NAV applied to pool. NAV increases skip PENDING and apply immediately.

Database Schema Overview

EmergeFi database — Supabase (PostgreSQL 15+) with RLSRow-Level Security — database-level access control that restricts which rows each user can see or modify. v2.13 · R15

🔐 Authentication 3 tables
👤 users
id UUID PK
email TEXT UNIQUE
role ENUM (GUEST | INVESTOR)
investor_tier ENUM (ACCREDITED | QP)
sumsub_applicant_id TEXT UNIQUE
kyc_level ENUM (KYC | KYB)
kyc_status ENUM (NOT_STARTED | PENDING | IN_PROGRESS | APPROVED | REJECTED | RETRY)
kyc_reviewed_at TIMESTAMPTZ
reject_type reject_type (RETRY | FINAL)
reject_reason TEXT
sbt_status sbt_status DEFAULT 'NOT_MINTED'
sbt_tx_hash TEXT
sbt_token_id INT
sbt_error TEXT
country TEXT
risk_level TEXT (LOW | MEDIUM | HIGH)
company_name TEXT (KYB only)
company_registration_number TEXT (KYB only)
company_country TEXT (KYB only)
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
R15: KYC/SBT columns added from merged kyc_submissions
🔑 auth_nonces
id UUID PK
wallet_address TEXT UNIQUE
nonce TEXT UNIQUE
expires_at TIMESTAMPTZ
used_at TIMESTAMPTZ
created_at TIMESTAMPTZ
💳 wallets
id UUID PK
user_id FK → users (CASCADE)
address TEXT
chain_id ENUM (8217 | 8453 | 11155111)
label TEXT
created_at TIMESTAMPTZ
UNIQUE(address, chain_id)
🛡️ Admin Management 3 tables
👑 admin_users
id UUID PK
email TEXT NOT NULL UNIQUE
name TEXT
avatar_url TEXT
wallet_address TEXT
role admin_role DEFAULT 'OPERATOR'
invite_code TEXT UNIQUE
invited_by FK → admin_users.id
last_active_at TIMESTAMPTZ
deleted_at TIMESTAMPTZ (soft delete)
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
Auth: Google OAuth. First admin seeded via env/DB.
🔒 admin_user_permissions
admin_user_id FK → admin_users (CASCADE)
page_key TEXT NOT NULL
granted BOOLEAN DEFAULT false
granted_by FK → admin_users.id
granted_at TIMESTAMPTZ
PK(admin_user_id, page_key)
Operators only; Admins have full access
📱 admin_sessions
id UUID PK
admin_user_id FK → admin_users (CASCADE)
device_info TEXT
ip_address INET
is_current BOOLEAN DEFAULT false
last_seen_at TIMESTAMPTZ
created_at TIMESTAMPTZ
My Settings → Security
🏢 Fund Management 2 tables
🏦 funds
id UUID PK
name TEXT NOT NULL
description TEXT
status fund_status DEFAULT 'ACTIVE'
primary_contact_name TEXT
primary_contact_email TEXT
notification_health TEXT DEFAULT 'HEALTHY'
verified BOOLEAN DEFAULT FALSE
established SMALLINT
total_originated NUMERIC(18,2) DEFAULT 0
website_url TEXT
logo_url TEXT
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
Renamed from issuers. Investor-side metadata: verified, established, etc.
👥 fund_members
id UUID PK
fund_id FK → funds (CASCADE)
admin_user_id FK → admin_users (nullable)
wallet_address TEXT NOT NULL
email TEXT
is_primary BOOLEAN DEFAULT false
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
UNIQUE(fund_id, wallet_address)
Renamed from issuer_admins. PK is UUID id, UNIQUE on (fund_id, wallet_address).
🏊 Pools 7 tables
🏊 pools
id UUID PK
name TEXT NOT NULL
description TEXT
pool_type pool_type (EF_POOL | FUND_POOL)
lifecycle_status lifecycle_status DEFAULT 'DRAFT'
is_paused BOOLEAN DEFAULT false
investment_blocked BOOLEAN DEFAULT false
fund_id FK → funds.id
target_apy NUMERIC(5,2) NOT NULL
tvl NUMERIC(18,2) DEFAULT 0
capacity NUMERIC(18,2) NOT NULL
min_investment NUMERIC(18,2) NOT NULL
term TEXT NOT NULL
risk_tier risk_tier NOT NULL
category pool_category NOT NULL
investor_tier investor_tier NOT NULL
asset_count INTEGER DEFAULT 0
chain_id chain_id NOT NULL
accepted_currencies currency[] DEFAULT '{USDC}'
nav_per_token NUMERIC(18,6) NOT NULL
reserve_percentage NUMERIC(5,2) DEFAULT 10.00
has_pending_nav_update BOOLEAN DEFAULT false
lp_issuance_model lp_issuance_model NOT NULL
escrow_model escrow_model
yield_distribution_model yield_distribution_model
penalty_type penalty_type NOT NULL
penalty_fee_amount NUMERIC
penalty_rate NUMERIC
lockup_days INTEGER DEFAULT 0
lockup_label TEXT NOT NULL
early_penalty TEXT
standard_redemption_days INTEGER DEFAULT 7
redemption_notes TEXT
start_date DATE
end_date DATE
display_after_close BOOLEAN DEFAULT true
published_at TIMESTAMPTZ
pool_address TEXT
escrow_address TEXT
receipt_address TEXT
lp_token_address TEXT
pool_wallet TEXT
total_yield_distributed NUMERIC DEFAULT 0
investor_count INTEGER DEFAULT 0
last_nav_update TIMESTAMPTZ
avg_asset_size NUMERIC(18,2)
avg_tenor TEXT
historical_default_rate NUMERIC(5,2)
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
CHK: nav_per_token_range, start_before_end
💱 pool_accepted_currencies
pool_id FK → pools (CASCADE)
currency_symbol TEXT NOT NULL
PK(pool_id, currency_symbol)
🔗 fund_pool_assignments
fund_id FK → funds (CASCADE)
pool_id FK → pools (CASCADE)
assigned_at TIMESTAMPTZ
PK(fund_id, pool_id)
📈 pool_tvl_history
id UUID PK
pool_id FK → pools (CASCADE)
tvl NUMERIC NOT NULL
recorded_at TIMESTAMPTZ DEFAULT now()
IDX: pool_id, recorded_at DESC
📊 pool_asset_compositions
id UUID PK
pool_id FK → pools (CASCADE)
label TEXT NOT NULL
percentage NUMERIC(5,2) NOT NULL
CHK: percentage 0~100
📄 pool_documents
id UUID PK
pool_id FK → pools (CASCADE)
name TEXT NOT NULL
type document_type NOT NULL
size TEXT
storage_url TEXT
docusign_view_url TEXT
created_at TIMESTAMPTZ
🏗️ underlying_assets
id UUID PK
pool_id FK → pools (CASCADE)
category TEXT
external_asset_id TEXT
borrower_name TEXT
principal_value NUMERIC(18,2)
maturity_date TIMESTAMPTZ
status asset_status DEFAULT 'ACTIVE'
last_synced_at TIMESTAMPTZ
💰 Deposits 1 table
📥 deposits
id UUID PK
pool_id FK → pools.id
user_id UUID (→ users.id)
wallet_id UUID (→ wallets.id)
receipt_code TEXT
amount NUMERIC NOT NULL
is_reinvestment BOOLEAN DEFAULT false
refund_eligible_at TIMESTAMPTZ (D+7)
matched_at TIMESTAMPTZ
investor_address TEXT
currency TEXT DEFAULT 'USDC'
status deposit_status DEFAULT 'PENDING'
received_at TIMESTAMPTZ
receipt_issued_at TIMESTAMPTZ (Fund Pool)
receipt_tx_hash TEXT
fm_notified_at TIMESTAMPTZ (Fund Pool)
lp_minted_at TIMESTAMPTZ
lp_mint_tx_hash TEXT
receipt_burned_at TIMESTAMPTZ (Fund Pool)
receipt_burn_tx_hash TEXT
failure_type TEXT
error_message TEXT
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
Renamed from pending_receipts IDX: pool, status, investor_address
💼 Portfolio 1 table
💼 portfolio_positions
id UUID PK
user_id FK → users.id
pool_id FK → pools.id
tokens NUMERIC(18,6) NOT NULL
effective_value NUMERIC(18,6) NOT NULL
accrued_yield NUMERIC(18,6) DEFAULT 0
invested_at TIMESTAMPTZ NOT NULL
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
UNIQUE(user_id, pool_id)
IDX: user_id, pool_id
🔄 Redemptions 1 table
🔄 redemption_requests
id UUID PK
pool_id FK → pools.id
user_id UUID (→ users.id)
position_id UUID (→ portfolio_positions.id)
nav_at_request NUMERIC(18,6)
payout_amount NUMERIC
penalty_amount NUMERIC DEFAULT 0
tx_hash TEXT
investor_address TEXT
amount NUMERIC NOT NULL
lp_tokens_amount NUMERIC
status redemption_status DEFAULT 'REQUESTED'
transfer_source transfer_source DEFAULT 'EMERGEFI'
payout_tx_hash TEXT → BaseScan
requested_at TIMESTAMPTZ
fm_notified_at TIMESTAMPTZ (Fund Pool)
fm_accepted_at TIMESTAMPTZ (Fund Pool)
contract_validated_at TIMESTAMPTZ (SC)
auto_released_at TIMESTAMPTZ (SC)
approval_requested_at TIMESTAMPTZ (Multi-sig)
cosigned_at TIMESTAMPTZ (Multi-sig)
fund_notified_at TIMESTAMPTZ (No escrow)
fund_sent_at TIMESTAMPTZ
transfer_confirmed_at TIMESTAMPTZ
lp_burned_at TIMESTAMPTZ
completed_at TIMESTAMPTZ
failure_type TEXT
error_message TEXT
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
IDX: pool, status, investor_address
📊 Yield 2 tables
💰 yield_distributions
id UUID PK
pool_id FK → pools.id
period TEXT (e.g. "Jan 2026")
total_amount NUMERIC NOT NULL
investor_count INTEGER DEFAULT 0
status yield_status DEFAULT 'PENDING'
tx_hash TEXT → BaseScan
fm_notified_at TIMESTAMPTZ (Fund Pool)
tx_submitted_at TIMESTAMPTZ
claimed_at TIMESTAMPTZ (Manual Claim)
failure_type TEXT
error_message TEXT
distributed_by FK → admin_users.id
distribution_type TEXT DEFAULT 'MANUAL'
apy_rate NUMERIC(5,2)
period_start TIMESTAMPTZ
period_end TIMESTAMPTZ
distributed_at TIMESTAMPTZ
created_at TIMESTAMPTZ
updated_at TIMESTAMPTZ
IDX: pool, status
👥 yield_distribution_investors
id UUID PK
distribution_id FK → yield_distributions (CASCADE)
user_id UUID (→ users.id)
investor_address TEXT
investor_name TEXT
share_percentage NUMERIC NOT NULL
amount NUMERIC NOT NULL
tx_hash TEXT
status yield_status DEFAULT 'PENDING'
created_at TIMESTAMPTZ
IDX: distribution_id
📊 NAV History 1 table
📊 nav_history
id UUID PK
pool_id FK → pools.id
old_nav NUMERIC(18,6) NOT NULL
new_nav NUMERIC(18,6) NOT NULL
source TEXT DEFAULT 'admin_override'
proposed_by FK → admin_users.id
proposed_at TIMESTAMPTZ DEFAULT now()
effective_at TIMESTAMPTZ NOT NULL
status nav_update_status DEFAULT 'PENDING'
reason TEXT
CHK: nav_positive (new_nav > 0), nav_max (new_nav <= 1.0)
🪪 KYC / Identity 1 table
📋 kyc_logs
id UUID PK
user_id FK → users (CASCADE)
provider_id TEXT
status_result TEXT (GREEN | RED)
risk_score DECIMAL
reviewed_at TIMESTAMPTZ
KYC/SBT fields merged into users table — kyc_logs tracks history only
🔔 Notifications 2 tables
📨 notification_logs
id UUID PK
recipient_id TEXT NOT NULL (polymorphic)
recipient_type recipient_type (ADMIN | FM | INVESTOR)
recipient_name TEXT
event_type TEXT NOT NULL
channel notification_channel DEFAULT 'EMAIL'
status notification_status DEFAULT 'DELIVERED'
failure_type notification_failure_type
error_message TEXT
retry_count INTEGER DEFAULT 0
notification_content TEXT
related_entity_type TEXT
related_entity_id UUID
created_at TIMESTAMPTZ
delivered_at TIMESTAMPTZ
IDX: status, recipient, entity
⚙️ notification_preferences
user_type TEXT NOT NULL ('ADMIN' | 'INVESTOR')
user_id UUID NOT NULL (polymorphic)
category TEXT NOT NULL
email_enabled BOOLEAN DEFAULT true
PK(user_type, user_id, category)
Categories: Critical, Operations, Reviews, Informational
Platform Config & Activity 2 tables
🔧 platform_config
key TEXT PK
value TEXT NOT NULL
updated_by FK → admin_users.id
updated_at TIMESTAMPTZ
Seeds: auto_refresh_interval, supported_stablecoins, block_explorer_base_url
📝 activity_events
id UUID PK
event_type TEXT NOT NULL
description TEXT NOT NULL
status_badge TEXT NOT NULL
actor_id UUID (→ admin_users.id)
metadata JSONB
related_entity_type TEXT
related_entity_id UUID
created_at TIMESTAMPTZ
IDX: created_at DESC · Dashboard shows last 48h
Enums Reference (30 types)
admin_role
OPERATORFUND_MANAGERADMIN
pool_type
EF_POOLFUND_POOL
lifecycle_status
DRAFTUPCOMINGACTIVECLOSEDDISTRESSEDMATURED
Replaces pool_status (R15)
escrow_model
SMART_CONTRACTMULTI_SIGNO_ESCROW
yield_distribution_model
DISTRIBUTINGMANUAL_CLAIM
lp_issuance_model
FUND_ISSUEDEMERGEFI_ISSUED
Renamed from AUTO/MANUAL
penalty_type
NO_EARLYFLAT_FEEPRINCIPAL_BASEDYIELD_BASED
deposit_status
PENDINGPROCESSINGCOMPLETEDREFUNDEDFAILED
redemption_status
REQUESTEDRECOMMENDEDFM_ACCEPTEDPROCESSINGCOMPLETEDREJECTEDFAILED
yield_status
PENDINGPROCESSINGDISTRIBUTEDFAILED
user_role
GUESTINVESTOR
risk_tier
12345
pool_category
INVOICEEWASTO
investor_tier
ACCREDITEDQP
chain_id
8217845311155111
Kaia · Base · Sepolia
document_type
PPMSUBSCRIPTIONAUDITRISK_DISCLOSURE
asset_status
ACTIVESETTLEDOVERDUE
currency
USDCUSDT
nav_update_status
PENDINGAPPLIED
kyc_level
KYCKYB
kyc_status
NOT_STARTEDPENDINGIN_PROGRESSAPPROVEDREJECTEDRETRY
reject_type
RETRYFINAL
sbt_status
NOT_MINTEDMINTEDFAILED
notification_channel
EMAIL
V1: Email only. Telegram/Slack V2.
notification_status
DELIVEREDFAILED
notification_failure_type
BOUNCEDSPAM_FILTEREDSERVER_ERRORTIMEOUTINVALID_RECIPIENTRATE_LIMITED
recipient_type
ADMINFMINVESTOR
transfer_source
EMERGEFIFUND
fund_status
ACTIVEINACTIVE

Views

dashboard_alert_counts
SELECT
  COUNT(*) FILTER (WHERE status='FAILED') FROM notification_logs  AS failed_notifications,
  COUNT(*) FILTER (WHERE sbt_status='FAILED') FROM users          AS sbt_mint_failed,
  COUNT(*) FILTER (WHERE status='FAILED') FROM deposits           AS failed_deposits,
  COUNT(*) FILTER (WHERE status='FAILED') FROM redemption_requests AS failed_redemptions,
  COUNT(*) FILTER (WHERE status='FAILED') FROM yield_distributions AS failed_yield,
  COUNT(*) FILTER (WHERE kyc_status='PENDING') FROM users         AS kyc_pending,
  COUNT(*) FILTER (WHERE status='REQUESTED') FROM redemption_requests AS pending_redemptions,
  COUNT(*) FILTER (WHERE status='PENDING') FROM yield_distributions AS pending_yield;
platform_stats
SELECT
  COALESCE(SUM(tvl), 0)                    AS total_tvl,
  COALESCE(SUM(total_yield_distributed), 0) AS total_yield,
  COALESCE(SUM(investor_count), 0)          AS total_investors
FROM pools
WHERE lifecycle_status = 'ACTIVE';
📖 Data Types Reference
PK Primary Key — Unique ID, no duplicates
FK Foreign Key — Links to another table's PK
UUID Unique ID — Random 128-bit, globally unique
ENUM Fixed Options — Only predefined values
TEXT Text — Variable-length string
NUMERIC Precise Number — Exact, used for money
BOOLEAN True / False — Yes or No flag
TIMESTAMPTZ Date + Time — Full timestamp with timezone
INTEGER Whole Number — No decimals
JSONB JSON Binary — Structured data, queryable
INET IP Address — IPv4 or IPv6
DATE Date Only — No time component

Relationship Map

users ←1:N→ wallets
users ←1:N→ kyc_logs
admin_users ←1:N→ admin_user_permissions
admin_users ←1:N→ admin_sessions
funds ←1:N→ fund_members ←N:1→ admin_users
funds ←M:N→ pools (via fund_pool_assignments)
pools ←1:N→ pool_accepted_currencies
pools ←1:N→ pool_tvl_history
pools ←1:N→ deposits
pools ←1:N→ redemption_requests
pools ←1:N→ yield_distributions ←1:N→ yield_distribution_investors
yield_distributions ←N:1→ admin_users (distributed_by)
admin_users ←1:N→ activity_events (actor_id)
auth_nonces — standalone (SIWE authentication)

Migrations

v2.13 · R15
22 tables · 20 enums · 2 views · polymorphic notifications · lifecycle_status (6-state) · admin/investor schema merge

Triggers, Auto-Rules & Business Logic

Automated database triggers, business rules, and system behaviors that run without manual intervention.

Database Triggers

🔓
No Safety Bounds — NAV Auto-Adjusts
PD6 (NAV Safety Bounds) removed. No per-transaction limit, no floor. NAV auto-adjusts on default events — the market price reflects the actual asset value. Only constraint: max $1.00 cap. Investment stays open at all NAV levels — new investors pay fair current price.
Refund Timer (7-Day Auto-Refund)
On investment creation, refund_eligible_at = now() + 7 days. If receipt_status is still PENDING at that time, system auto-refunds USDC from escrow to investor. Receipt Token → REFUNDED.
🔒
NAV Decrease Timelock (24 Hours)
When Oracle posts a NAV decrease, it's logged in nav_history with effective_at = now() + 24h and status PENDING. The actual pools.nav_per_token only updates after the timelock expires. NAV increases (recovery) bypass the timelock and apply immediately. Gives stakeholders time to react before a lower price takes effect.
🔗
LP Transfer Auto-Settlement
Before any LP token transfer (Model B), unclaimed yield is auto-settled to the sender. Prevents yield loss on token movement.

Business Rules

🚫
Investment Pre-Checks
Before allowing deposit: kyc_status = APPROVED AND is_paused = false. Investment stays open regardless of NAV level — new investors always pay the current fair price. There is no investment_blocked auto-trigger — the NAV model never auto-blocks investment.
1️⃣
One Active Redemption Per Investment
System enforces unique constraint: only one non-terminal (not COMPLETED/REJECTED) redemption request per investment_id. Investor must wait for resolution before requesting again.
📸
NAV Snapshot on Redemption Request
nav_at_request is captured from pools.nav_per_token at request creation and never recalculated. Payout = tokens_minted × nav_at_request + accrued_yield. Protects investor from further NAV drops during processing. Also means investor doesn't benefit from NAV recovery after request.
📐
Yield on Nominal Balance
Accrued yield always calculated on nominal (original) investment amount), not NAV-adjusted token value. Formula: nominal_amount × (APY / 365) × days_held. This incentivizes investors to stay through NAV dips — they still earn full yield on their original deposit.
📋
FIFO Redemption Queue
Redemption requests processed oldest-first. Admin checks pool liquidity before approving. If insufficient liquidity, request stays in queue (not rejected). Admin can skip FIFO only if rejecting with reason.
⚖️
Early Redemption Penalty
If investor redeems during lockup period: per-pool configurable penalty applies (e.g., 50% of accrued yield forfeited). Penalty amount determined at pool creation by Admin.

Pool Visibility Rules

🏠 Dashboard (Public)
Shows only pools where is_paused = false. NAV below par does not hide a pool — investment stays open and price reflects fair value. Exception: user has an active position → pool always visible regardless of status.
📂 Portfolio (Investor)
Shows ALL user investments regardless of pool status. Paused, writedown, even offboarded pools still display with appropriate status badges.
⚙️ Admin Panel
Admin/Operator see all pools (including paused). Fund Manager only sees pools under their assigned issuer(s). Oracle health status visible to Admin + Operator via /admin/oracle.

Decisions

Open items for business, product, and legal alignment. Click any card to expand and see full options. Resolve decisions interactively.

🟡 Business Decisions

These shape the product's financial and operational model. Each has a recommendation but is open for discussion.

🔵 Product Decisions

Technical architecture choices. Some are blocked until a related Business Decision is resolved.

Regulatory and legal structure decisions. Require legal counsel before finalizing.

API Reference

All backend API endpoints grouped by domain. Status: Built   Planned   NAV-dependent endpoints. 15 built, 10+ planned.

Investments

POST/api/investments/initiate
Create investment record, mint receipt token, lock USDC in escrow
GET/api/investments/{id}
Get investment details including LP status and receipt token
GET/api/investments?userId={id}
List all investments for a user
PATCH/api/investments/{id}/verify-lp
Admin: mark LP as verified, release USDC from escrow (Model A)

Pools

GET/api/pools
List all active pools (with status, NAV, APY, capacity)
GET/api/pools/{id}
Get pool details including NAV history
POST/api/admin/pools
Admin: create new pool with config (APY, lock-up, reserve %, etc.)
PATCH/api/admin/pools/{id}/nav
Admin: update NAV (triggers 24h timelock if decrease). Planned.

Redemptions

POST/api/redemptions
Investor: submit redemption request. Locks NAV snapshot.
GET/api/redemptions?investmentId={id}
List redemption requests for an investment
PATCH/api/redemptions/{id}/recommend
Operator: recommend approval (checks liquidity)
PATCH/api/redemptions/{id}/approve
Admin: final approval + USDC payout

Auth & KYC

POST/api/auth/siwe
Sign-In With Ethereum — returns JWT on valid signature
GET/api/kyc/status
Check KYC status for current wallet address
POST/api/kyc/hook
SumSub webhook — updates kyc_status and mints SBT on approval
GET/api/auth/role
Role check on route load — returns user’s platform role (Admin/Operator/Fund Manager)

Yield

POST/api/admin/pools/{id}/yield
Operator: record yield distribution for a pool. Planned — depends on BD1.
GET/api/pools/{id}/yield-history
Get yield distribution history for a pool. Planned — depends on BD1.

Issuers (Admin)

POST/api/admin/issuers
Admin: create new issuer. Planned.
PATCH/api/admin/issuers/{id}
Admin/Operator: edit issuer details. Planned.
GET/api/admin/issuers
Admin: list all issuers. Planned.

NAV & Oracle

GET/api/pools/{id}/nav-history
Investor: NAV change history for a pool. Planned.
GET/api/admin/oracle
Admin: oracle health status and last update times. Planned.

Admin Management

POST/api/admin/roles
Admin: assign role (Operator or Fund Manager) to a wallet. Planned.
DELETE/api/admin/roles/{id}
Admin: remove role assignment. Planned.

Product Timeline

Development phases and status. Based on product spec as of Feb 2026.

✓ Done
Core smart contracts (6 contracts deployed on Base Sepolia)
SIWE authentication + JWT
SumSub KYC integration + SBT minting
Pool creation & management (admin)
Investment initiation + escrow
Investor portfolio view
Redemption request flow
Two-step admin approval queue
RBAC (Admin / Operator / Fund Manager)
▶ In Progress
NAV update flow (admin panel)
24h timelock enforcement
Portfolio NAV display (investor UI)
Writedown UX (tone + transparency)
Admin panel V2 cleanup
○ Planned
DocuSign term sheet integration
NAV oracle contract
Email notification system
Reserve fund mechanics
KAIA mainnet dual-deploy
Multi-sig admin controls
❓ Open / TBD
Secondary market / LP transfers
Multi-asset support (USDT?) — decision removed Feb 19
Investor onboarding (KYB flow)
Analytics dashboard
Mobile app

Changelog

Architecture decisions and doc updates over time. Most recent first.

📅 February 2026 5 updates
Feb 20, 2026 — PD1–PD4 decided + Gap report fixes (v6)
Oracle decisions finalized: PD1 integrated into EmergeFiPool, PD2 ORACLE_ROLE, PD3 EmergeFi-operated, PD4 no fallback (admin pool pause for emergencies). Added ORACLE_ROLE to contracts. 13 missing DB fields added. yield_distributions placeholder table added. NAV Update status machine added. 10+ planned API endpoints. Removed NAV Emergency Override. Phase 3 Redemption added to Investment Lifecycle.
🔄
Feb 19, 2026 — Product spec sync + safety bounds simplified
PD6 (NAV Safety Bounds) removed — no floor, no per-tx limit, NAV capped at $1.0 max only. PD1 recommendation updated: Integrated into EmergeFiPool is now recommended over separate oracle contract. Tech stack verified from GitHub repo. API endpoints: 15 built, rest planned.
🔄
Feb 19, 2026 — Decisions System Overhaul (Batch 5)
Upgraded DECISIONS data model to BD1–BD6 + PD1–PD5 with dependsOn, blocks, referencedIn schema. Merged Business Decisions + Product Decisions into unified Decisions page. Side panel now shows dependency graph.
🗂️
Feb 11, 2026 — Docs Site Restructure (Batch 4)
Converted horizontal tab navigation to sidebar navigation docs site. Added: Overview & Architecture, Core Concepts, Smart Contracts, API Reference, Decisions, Timeline, and Changelog sections. Split Writedown & Redemption into two separate sections. Added decision badge system with slide-out panels.
📐
Feb 15, 2026 — NAV Model Migration (v5)
Replaced Principal Factor (PF) multiplier with NAV (nav_per_token) as the single loss indicator. Investment is no longer auto-blocked when NAV < $1.00. New investors pay fair current price. Writedown formula updated throughout. Removed investment_blocked from pools entity.
📅 January 2026 2 updates
👥
Jan 2026 — RBAC Update: Fund Manager Role Added (v4)
Added Fund Manager as a third role (read-only, pool-scoped). Clarified Admin vs Operator split. Updated permission matrix. — Batch 1
Jan 2026 — Two-Step Redemption Queue Confirmed (v3)
Redemption approval changed from single-admin to Operator-recommends + Admin-approves. FIFO queue enforced. NAV snapshot locked at request time.
📅 December 2025 1 update
🚀
Dec 2025 — Initial Backend Logic Map Published (v1)
First version with Investment Lifecycle, Status Machines, Database Schema, and Business Decisions. Deployed to Vercel via GitHub integration.