Flex Engineers
5 min read

Forge Finance: Building a Multi-Chain NFT Marketplace with a TikTok-Style Reel

SolidityNext.jsWeb3PostgreSQLBlockchain DevelopmentSmart Contract Development

What it is

Forge Finance is a full-stack NFT marketplace: mint, list, buy, and sell image and video NFTs across multiple chains, with wallet-based auth, a Sequelize/Postgres backend, and a TikTok-style vertical video reel ("Forge Reel") for browsing NFTs, chat between buyers and creators, and per-collection royalty splits enforced on-chain. Our team built both the client (Next.js) and the server (Express) from scratch and shipped it to a live domain (dev.forgefinance.io).

It's a two-repo system:

  • forge-finance-client-v2 — Next.js 12 + React 18, Chakra UI + Tailwind, Web3.js/Web3Modal for wallet connections, Truffle-managed Solidity contracts.
  • forge-finance-server — Express + Sequelize on Postgres, JWT auth, Socket.IO for real-time messaging, Nodemailer/Google APIs for email verification.

The core problem: who actually owns the transaction logic

Most tutorial-grade NFT marketplaces put too much trust in the client and too little in the contract, or they push everything on-chain and make simple reads expensive. Forge Finance splits the concern deliberately:

  • On-chain (Marketplace.sol): item listing, escrow, sale settlement, and royalty distribution. This is the part that touches money — it has to be trustless.
  • Off-chain (Postgres via Sequelize): metadata, likes, views, collections, comments, chat — anything that benefits from fast queries and doesn't need consensus.

The marketplace contract is a single ReentrancyGuard-protected contract shared across every collection, rather than one marketplace per collection:

struct MarketItem {
    uint256 itemId;
    address nftContract;
    uint256 tokenId;
    address payable seller;
    address payable owner;
    uint256 price;
    bool sold;
    Categories categories;
}

function createMarketSale(address nftContract, uint256 itemId)
    public payable nonReentrant
{
    MarketItem storage marketItem = _idToMarketItem[itemId];
    require(msg.value >= marketItem.price, "msg.value must be equal greater than price");
    require(address(this) == IERC721(nftContract).ownerOf(marketItem.tokenId),
        "Item is not listed in the marketplace");

    uint256 royaltyFee = (msg.value * tokenToCreatorStruct[nftContract][marketItem.tokenId].royalty) / 100;
    marketItem.seller.transfer(msg.value - (2 * royaltyFee));
    tokenToCreatorStruct[nftContract][marketItem.tokenId].creator.transfer(royaltyFee);
    payable(owner()).transfer(royaltyFee);

    IERC721(nftContract).safeTransferFrom(address(this), msg.sender, marketItem.tokenId);
    _itemsSold.increment();
}

Each ERC-721 collection is its own NFT_URIStorage contract deployed independently, but they all sell through the same Marketplace singleton. That means one gas-optimized contract handles listing, escrow, and payout for every collection on the platform, and the mapping tokenToCreatorStruct[nftContract][tokenId] remembers the original creator forever — so royalties still get paid correctly on the fifth resale, not just the first.

Every sale splits into three transfers in one atomic transaction: seller proceeds, creator royalty (capped at 10%, enforced in createMarketItem), and a platform cut to the contract owner. No off-chain reconciliation job, no "trust us, we'll pay you later."

Minting flow: IPFS, not a CDN

NFT assets (image/video files and their JSON metadata) go to IPFS via Infura's node, not to platform-controlled storage:

const client = ipfsHttpClient("https://ipfs.infura.io:5001/api/v0");

const added = await client.add(new File([blob], file?.name));
const url = `https://ipfs.infura.io/ipfs/${added.path}`;

const metaData = JSON.stringify({ url, name, description, price });
const addedMeta = await client.add(metaData);
const metaUrl = `https://ipfs.infura.io/ipfs/${addedMeta.path}`;

The token URI stored on-chain points at the IPFS metadata hash, not a mutable API endpoint — the whole point of an NFT is that the "N" means something, and that only holds if the asset can't quietly change out from under the token.

Postgres still tracks a mirror of every NFT (minted: boolean, likes, views, collectionId) so the marketplace UI can do things a smart contract is bad at: fast search, feed ranking for the reel, like/view counters that update without a wallet signature. The minted flag exists specifically because creators can save a draft NFT before ever touching gas — mint-on-demand rather than forcing a wallet signature at creation time.

Real-time chat over Socket.IO, backed by the same Postgres tables

Buyers and creators message each other in-app. Rather than a separate pub/sub layer, the server persists every message to Postgres and broadcasts to a Socket.IO room keyed by conversation ID:

socket.on('send_message', async data => {
  await Message.create({ message: data.message, conversationId: data.room, userId: data.myid });
  await Conversation.update({ lastUpdated: Date.now() }, { where: { id: data.room } });

  const messages = (await Message.findAll({ where: { conversationId: data.room } }))
    .map(m => m.dataValues);

  socket.to(data.room).emit('receive_message', messages);
});

This trades some throughput ceiling for simplicity and consistency: there's one source of truth (Postgres), the socket layer is purely a delivery mechanism, and a page refresh never loses a message because nothing lives only in memory.

Multi-chain by address swap, not by rewrite

The client supports both Ethereum testnets and BSC from the same codebase. Rather than branching UI logic per chain, it swaps contract addresses based on the connected chainId:

async function createMarketItem(tokenId, price) {
  if (chainId == 4) {
    nftaddress = nftaddressETH;
    nftmarketaddress = nftmarketaddressETH;
  }
  // ... BSC branch swaps in the BSC-deployed addresses
}

Same ABI, same UI, same transaction flow — only the deployed addresses change, since the same Marketplace.sol/NFT.sol pair is deployed independently to each network via Truffle migrations. That's what let a small team support two chains without doubling the frontend surface area.

Forge Reel: NFTs as a swipeable feed

The forgeReel boolean on the NFT model is a small piece of schema doing a lot of product work — it's what turns a flat NFT table into a curated, TikTok-style vertical feed (/forge-reel) separate from the standard grid marketplace. Creators opt individual video NFTs into the reel at mint time; the reel endpoint just filters WHERE forgeReel = true ORDER BY id DESC and stitches in creator profile data. It's a good example of solving a discovery problem with a boolean column and a sort order instead of a new subsystem.

Stack summary

LayerChoiceWhy
ContractsSolidity 0.8.4, OpenZeppelin ERC-721 + ReentrancyGuardBattle-tested primitives, custom marketplace logic only where it adds value
Chain toolingTruffle, Web3.js, Web3Modal (+ WalletConnect)Broad wallet support without forcing MetaMask
Asset storageIPFS via InfuraContent-addressed, so token URIs stay honest
APIExpress + Sequelize/PostgresRelational data (users, collections, likes) doesn't belong on-chain
Real-timeSocket.IO, backed by PostgresSimple delivery layer over a durable store, not a second database
FrontendNext.js 12, Chakra UI + TailwindFast iteration, SSR where it matters for marketplace SEO

Takeaway

Forge Finance is the clearest example in our portfolio of drawing the line correctly between on-chain and off-chain responsibility: the contract owns money movement and provenance, Postgres owns everything that benefits from being fast and mutable, and neither pretends to do the other's job. That discipline — plus reusing one marketplace contract across every collection instead of deploying redundant logic per creator — is what let this ship as a real, multi-chain product rather than a single-network demo.