What began as a mandatory final project for Course 02369 at DTU quickly spiraled out of control. CampusCred started with a simple objective: build a system to replace the archaic, easily forged PDF transcripts used by universities. It evolved into a 4,400-line full-stack prototype that bridges the gap between blockchain immutability and GDPR-compliant privacy.
While the "school project" requirements were modest, I wanted to solve the real-world friction of academic verification: the slow, manual "trust-but-verify" loop that recruiters hate.
The Architecture: A Hybrid Approach
One of the biggest misconceptions about blockchain credentials is that everything goes "on-chain." That is a privacy nightmare. We designed a Hybrid Architecture that leverages the best of both worlds:
- On-Chain (Public Trust): We deployed an ERC-721 smart contract on the Ethereum Sepolia testnet. This holds only non-sensitive data: the Token ID, the Issuer Address, and a cryptographic hash of the evidence.
- Off-Chain (Private Evidence): The actual transcripts, student names, and grades live in a secure, access-controlled layer (AWS S3 or Local Storage).
To keep the codebase clean, we implemented a strict Service Layer Pattern in the Flask backend. Controllers (Routes) never talk to the database or blockchain directly. Instead, they delegate to specialized services:
# backend/app/services/blockchain.py
class BlockchainService:
def mint_credential(self, student_address, metadata_uri):
"""
Orchestrates the minting process via Web3.py.
Handles nonce management and gas estimation automatically.
"""
txn = self.contract.functions.safeMint(
student_address,
metadata_uri
).build_transaction({
'chainId': self.chain_id,
'gas': 2000000,
'nonce': self.w3.eth.get_transaction_count(self.deployer_address),
})
# Sign and send transaction
signed_txn = self.w3.eth.account.sign_transaction(txn, self.private_key)
return self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
Smart Contract: The "Soulbound" Logic
Standard NFTs are tradeable assets. Academic degrees are not. If a student can sell their degree on OpenSea, the system is broken. We implemented Soulbound Tokens by overriding the standard OpenZeppelin transfer logic.
Using the `_update` hook (modern OpenZeppelin syntax), we ensure tokens can only move during minting (`from == 0`) or burning (`to == 0`). Any other transfer attempts trigger a revert.
// contracts/CampusCred.sol
function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
address from = _ownerOf(tokenId);
// Allow Minting (from 0x0) and Burning (to 0x0)
// Block all other transfers
if (from != address(0) && to != address(0)) {
revert("CampusCred: Credentials are non-transferable");
}
return super._update(to, tokenId, auth);
}
The "Wallet-Free" Verification Challenge
The biggest barrier to Web3 adoption is UX. A recruiter will not install Metamask just to verify a candidate. We solved this with a Selective Disclosure System.
The student generates a temporary "Verifier Link" (valid for 15 minutes). The backend validates the student's wallet signature, generates a secure token, and serves a specialized view. This allows the recruiter to view PII and download a digitally signed PDF without ever touching a blockchain wallet.
Engineering Around Limitations: Headless Wallet Injection
This is where the project went deep. Testing a DApp (Decentralized App) end-to-end is notoriously difficult. Standard headless browsers (Chromium) controlled by Playwright do not support browser extensions like Metamask. This meant we couldn't easily automate the "Connect Wallet" or "Sign Transaction" flows.
We engineered a Playwright Injection Bypass. Instead of trying to control a GUI extension, we inject a mock Ethereum provider directly into the browser's `window` object before the app loads.
# tests/e2e/conftest.py
@pytest.fixture
def browser_context(playwright):
browser = playwright.chromium.launch(headless=True)
context = browser.new_context()
# Inject the Mock Provider Shim
# This tricks the frontend into thinking Metamask is present
context.add_init_script("""
window.ethereum = {
isMetaMask: true,
request: async ({ method, params }) => {
if (method === 'eth_requestAccounts') {
return ['0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266']; // Hardhat Test Account
}
if (method === 'personal_sign') {
return '0xdeadbeef...'; // Mock signature
}
console.log('Mock Provider intercepted:', method);
return null;
}
};
""")
return context
This bypass allowed us to achieve 74% test coverage, verifying the full user journey: from clicking "Connect Wallet" to Minting, in a headless CI/CD environment without manual intervention.
Future Roadmap
While the prototype is robust, the next steps involve deepening the trust layer:
- MitID Integration: Moving from wallet-based auth to identity-based auth.
- Zero-Knowledge Proofs (ZKP): Allowing students to prove a GPA > 3.0 without revealing the exact number.
Final Thoughts
CampusCred proves that with the right architecture, we can abstract away the complexity of blockchain while keeping its integrity. It was a "school project" that forced us to solve real engineering problems: from gas optimization to headless browser injection.