Mersi

Database

PostgreSQL schema for backend/ — table relationships, key constraints, and migration workflow.

The backend/ service uses PostgreSQL via Drizzle ORM and the postgres.js driver. All schema is defined as TypeScript in backend/src/db/schema/.

Tables

users

Stores user profiles, wallet addresses, and onboarding progress.

ColumnTypeNotes
idUUIDPrimary key
crossmint_user_idvarchar(255)UNIQUE — Crossmint identity
emailvarchar(320)Indexed
wallet_addressvarchar(66)Sui wallet address (indexed)
crossmint_wallet_idvarchar(255)Crossmint wallet reference
wallet_statusvarchar(20)none | pending | active | failed
onboarding_stepinteger0–3
display_namevarchar(100)Set in onboarding step 1
first_name, last_namevarchar(50)Set in onboarding step 2
street, apt, city, state, zip, countryvarcharShipping address (step 2)
tops_size, bottoms_size, footwear_sizevarchar(10)Sizes (step 3)
evm_addressvarchar(42)Crossmint-provisioned EVM wallet address
sui_private_key_encryptedtextAES-encrypted Sui key (optional)
created_at, updated_attimestampAudit timestamps

chat_sessions

Chat conversation threads.

ColumnTypeNotes
idUUIDPrimary key
user_idUUIDFK → users.id (cascade delete)
titlevarchar(100)Display name — auto-generated on first exchange
created_at, updated_attimestampBoth indexed for ordering

chat_messages

Individual messages within a session.

ColumnTypeNotes
idUUIDPrimary key
session_idUUIDFK → chat_sessions.id (cascade delete)
msg_idvarchar(100)AI SDK UIMessage.id
rolevarchar(20)user | assistant | system | tool
partsJSONBUIMessage.parts[] array — text, tool-invocation, etc.
created_attimestampIndexed by session_id

cart_items

Off-chain cart items (active when CART_SERVICE=db; also mirrored from Sui events when onchain).

ColumnTypeNotes
idUUIDPrimary key
user_idUUIDFK → users.id (cascade delete)
product_idvarchar(255)Retailer-scoped product ID
product_namevarchar(500)
priceintegerIn USD cents
image, product_urlvarchar(2048)
size, colorvarchar(50)
retailervarchar(255)
tx_digestvarchar(255)Sui tx that added the item (onchain mode)
on_chain_object_idvarchar(255)UNIQUE — Sui object ID of the cart item
deleted_attimestamp (with tz)Soft delete (null = active)
created_attimestamp

Unique constraint: (user_id, product_id, size, color) WHERE deleted_at IS NULL — prevents duplicates in the active cart while allowing re-add after checkout.

orders

Payment orders — both checkout and deposit types.

ColumnTypeNotes
idUUIDPrimary key
user_idUUIDFK → users.id (cascade delete)
typevarchar(20)checkout | deposit
crossmint_order_idvarchar(255)UNIQUE — Crossmint order reference
statusvarchar(50)awaiting_approval / payment_confirmed / in_progress / delivered / cancelled
amount_usdcnumericTotal charge in USDC
tx_hashvarchar(255)UNIQUE — Sui PTB digest (on-chain checkout) or deposit tx
payment_hashvarchar(255)EVM payment tx ID from Crossmint webhook
created_attimestamp

order_items

Product snapshot captured at checkout time. One row per order.

ColumnTypeNotes
idUUIDPrimary key
order_idUUIDFK → orders.id (cascade delete)
cart_item_idUUIDFK → cart_items.id (set null on delete)
product_idvarchar(255)
product_namevarchar(500)
priceintegerIn cents at time of checkout
image, product_urlvarchar(2048)
size, colorvarchar(50)
retailervarchar(255)
on_chain_object_idvarchar(255)Sui cart item object ID
created_attimestamp

Relationships

Migrations

Migrations live in backend/src/db/migrations/ as plain SQL files.

# Run from the backend/ directory
cd backend

# Generate a new migration from schema changes
bunx drizzle-kit generate

# Apply pending migrations manually
bunx drizzle-kit migrate

# Browse DB in a visual UI
bunx drizzle-kit studio

Migration files are named NNNN_description.sql and tracked in meta/_journal.json. Never modify existing migration files — add new ones instead.

Connection Pools

Two database connections are maintained:

ClientConfigUsage
queryClientmax 5 connectionsAll query operations (Drizzle db)
migrationClientmax 1 connectionMigration runner only

DATABASE_URL_DIRECT bypasses PgBouncer for the migration client — required when using Neon's pooler endpoint.

How is this guide?

On this page