This is a crowdfunding website, like FundMe, but built on the blockchain.
Anyone can create a campaign asking for money. Other people can then give money (called "contributing") to campaigns they like. All the important rules (how much has been raised, who can take the money out, when to give refunds) live inside a smart contract on the blockchain. This means no one, not even the person running the website, can secretly touch the money.
People can contribute using ETH (the normal currency of Ethereum) or an ERC20 token (a different kind of coin that lives on the same blockchain, like a stablecoin). The campaign creator picks which one(s) they accept.
- Create a campaign and choose how people can pay: ETH only, Token only, or Both.
- Contribute to a campaign using ETH or an ERC20 token.
- Money is locked in the smart contract. It can only be taken out ("withdrawn") by the campaign owner, and only after the goal is reached.
- If a campaign fails to reach its goal in time, contributors can get a refund, in the exact same currency they paid with.
- Log in with email/Google (using Auth0), then connect your crypto wallet.
- Add a title, description, cover photo, category, and country to each campaign.
- See a live list of contributors and every transaction (contribute, withdraw, refund) on a campaign.
- Comment on a campaign and reply to other people's comments.
- Archive a campaign once it's finished.
- Watch live logs and errors from the backend, the comment service, and even the website itself (in the browser), using Grafana.
contract/– the smart contract (written in Solidity, using the Foundry tool)backend/– a Go API that reads the blockchain and stores extra info (like descriptions and photos) in a databasecomment/– a small Go service that only handles comments, talks to the backendfrontend/– the actual website (built with React and Vite)
Set things up in this order: contract first, then backend, then frontend. Each part depends on the one before it.
The smart contract is the "source of truth" — it holds the real money and the real rules. Everything else (backend, frontend) just reads from it or sends transactions to it.
We use Foundry to build, test, and deploy it. Install Foundry first if you don't have it.
cd contract
forge build
forge testNow let's run a fake, private blockchain on your own computer to test on (this is called Anvil, and it comes with Foundry):
-
Open a new terminal and run:
anvil
This starts a fake blockchain and prints out 10 test accounts, each with 10,000 fake ETH and a private key. Keep this terminal open. Copy the private key of "Account #0" — you'll use it as the deployer.
-
Copy the example env file and fill it in:
cp .env.example .env
RPC_URL— leave ashttp://127.0.0.1:8545(this is Anvil's address)PRIVATE_KEY— paste the private key you copied above
-
Deploy the contract:
forge script script/DeployProxy.s.sol:DeployProxyScript --rpc-url anvil --broadcast
This prints two addresses. Copy the one called
proxyAddress— that's the address you'll use everywhere else (backend, frontend, MetaMask). You can ignoreimplementationAddress. -
(Optional, only needed if you want to test paying with a token) Deploy a fake test token:
forge script script/DeployMockToken.s.sol:DeployMockTokenScript --rpc-url anvil --broadcast
This creates a fake stablecoin called
tUSDCand gives 1,000,000 tUSDC to each of the 10 Anvil test accounts. Copy the token address that gets printed — you'll need it later.
If you change anything in the contract, rebuild it and regenerate the files the backend and frontend use to talk to it:
forge build
jq '.abi' out/CrowdFunding.sol/CrowdFunding.json > ../frontend/src/contract/CrowdFundingAbi.json
jq '.abi' out/CrowdFunding.sol/CrowdFunding.json > ../backend/contract/CrowdFunding.abi.json
cd ../backend/contract
abigen --abi=CrowdFunding.abi.json --pkg=contract --type=CrowdFunding --out=crowdfunding.goSo you can actually click around the website with a real wallet, add your local Anvil blockchain to MetaMask (or any wallet you use):
- Open MetaMask → click the network dropdown → Add network → Add a network manually.
- Fill in:
- Network name: anything you like, e.g.
Anvil Local - New RPC URL:
http://127.0.0.1:8545 - Chain ID:
31337 - Currency symbol:
ETH
- Network name: anything you like, e.g.
- Save, then switch to this new network.
- Import a test account: click the account icon → Import Account → paste one of the private keys Anvil printed (pick a different one than the deployer, e.g. "Account #1", so you have someone who can act as a contributor). Now MetaMask has fake ETH (and fake tUSDC, if you deployed it) ready to use.
The backend reads campaign data straight from the blockchain, and keeps extra info in its own database (titles, descriptions, photos, comments, login sessions, etc. — things that don't need to live on the blockchain).
-
Copy the example env file:
cd backend cp .env.example .env -
Fill in
.env:RPC_URL— same as the contract,http://127.0.0.1:8545CONTRACT_ADDRESS— theproxyAddressyou copied during contract setupJWT_SECRET— any long random text (used to sign login sessions)SCOPE_MASK_SECRET— any long random text (used to hide real database IDs from the outside world)POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB/POSTGRES_PORT— fine to leave as the defaultsCOMMENT_SERVICE_ADDR/COMMENT_SERVICE_TOKEN— fine to leave as the defaults for local useSUPPORTED_TOKENS— optional, only needed for token campaigns (see the testing section below)
-
Set up login (Auth0): the website uses Auth0 (a free login service) so people can sign in with email/Google/etc.
- Sign up for a free Auth0 account.
- Create an Application (type: Single Page Application).
- Create an API (Applications → APIs → Create API) — give it any
name and an identifier like
https://crowdfunding-api. This identifier is the "audience". - From the Application's settings, copy the Domain and Client ID.
- Put these into
.env:AUTH0_APP_DOMAIN(the Domain),AUTH0_AUDIENCE(the API identifier you made up). - You'll reuse the Domain, Client ID, and audience again in the frontend setup below.
-
Set up photo storage (Cloudflare R2): campaign cover photos are stored on Cloudflare R2 (cheap, S3-like storage — the free tier is enough for testing).
- Sign up for a free Cloudflare account and turn on R2.
- Create a bucket (any name).
- Create an API token for R2 with read/write access — this gives you an Access Key ID and Secret Access Key.
- Turn on public access for the bucket (or connect your own domain) so photos can be viewed from a browser — this gives you a Public URL.
- Put these into
.env:R2_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_BUCKET,R2_PUBLIC_URL, andR2_S3_API(this looks likehttps://<your-account-id>.r2.cloudflarestorage.com).
-
Start everything with Docker (this starts Postgres, the backend, and the comment service together):
cd .. docker compose up -d --buildThe backend automatically creates its database tables on startup, and reaches your Anvil node (running on your computer, not in Docker) through
host.docker.internalinstead of127.0.0.1. -
Check it's alive:
curl http://localhost:8080/health
If you'd rather run the backend directly on your computer instead of in Docker (useful while making code changes), run just Postgres in Docker and the backend on its own:
docker compose up -d postgres go run .
This project also comes with Grafana + Loki, so you can see live logs from the backend, the comment service, and even errors from people's browsers, all in one place.
docker compose up -d loki promtail grafana alloyOpen http://localhost:3000 and go to Explore
→ pick the Loki datasource. Try a query like {service="backend"} or
{app_name="crowdfunding-frontend"}.
-
Copy the example env file:
cd frontend cp .env.example .env -
Fill in
.env:VITE_CONTRACT_ADDRESS— the sameproxyAddressas the backendVITE_API_BASE_URL—http://localhost:8080VITE_AUTH0_DOMAIN/VITE_AUTH0_CLIENT_ID/VITE_AUTH0_AUDIENCE— the same Domain, Client ID, and audience from the Auth0 application you made in the backend stepVITE_FARO_URL— only needed if you set up Grafana above, leave ashttp://localhost:12347/collect
-
In your Auth0 Application's settings, add these so login actually works:
- Allowed Callback URLs:
http://localhost:5173/auth/callback - Allowed Logout URLs:
http://localhost:5173 - Allowed Web Origins:
http://localhost:5173
- Allowed Callback URLs:
-
Install and run:
npm install npm run dev
-
Open http://localhost:5173 in the same browser where you set up your MetaMask test account.
Once everything above is running, here's how to try out all three ways of paying: ETH only, Token only, and Both.
-
Make sure you deployed the fake token (contract setup, step 4) and put its address into the backend's
SUPPORTED_TOKENS, like this:SUPPORTED_TOKENS=[{"symbol":"tUSDC","address":"<token address here>","decimals":6}]Restart the backend after changing this.
-
Log in on the website and go to Create Campaign.
-
Fill in the basic details, then in the Funding section, pick one:
- ETH — you only set an ETH goal. People pay with ETH.
- Token — you pick a token from the list and set a token goal. People pay with that token.
- Both — you set both an ETH goal and a token goal. People can pay with either one.
-
Click Publish. MetaMask will pop up once, asking you to confirm creating the campaign. This always costs a small ETH gas fee, even for token campaigns — gas is a separate thing from the campaign's currency, and it's always paid in ETH.
-
Now try contributing (open the campaign page and use the "Contribute" box):
-
ETH: MetaMask asks you to confirm once, and you're done.
-
Token: MetaMask asks you to confirm twice:
- Approve — lets the app move that amount of your tokens.
- Contribute — actually sends the tokens.
(This is normal for ERC20 tokens. If you contribute again with a different amount later, you'll likely see the "Approve" step again.)
-
Both: you'll see a small switch to pick ETH or the token before typing in an amount.
-
-
Don't have any test tokens? The
DeployMockTokenscript already gave 1,000,000 tUSDC to each of the 10 default Anvil accounts. If you're using a different account, the test token'smintfunction is open to anyone, so you (or the app) could give any address more of it — it's fake money for testing only. -
Withdraw: once the ETH goal or the token goal is reached (whichever happens first), the campaign owner can click Withdraw Funds and receive everything raised so far, in whichever currencies were used.
-
Refund: let the campaign's deadline pass without reaching either goal. On Anvil, you can skip time forward yourself instead of waiting:
cast rpc evm_increaseTime 604801 cast rpc evm_mine
After that, contributors can request a refund, and they always get back the exact same currency they originally paid with.