SelfBackendVerifier
The SelfBackendVerifier
class is the backend component that validates zero-knowledge proofs generated by the Self mobile app. It acts as the "proof validator" in your verification flow, ensuring that users have provided valid identity document information according to your requirements.
How Backend Verification Works
When a user scans your QR code and provides their identity document information, here's what happens on the backend:
1. Proof Reception
Self mobile app sends a zero-knowledge proof to your API endpoint
The proof contains cryptographic evidence that the user meets your requirements
No actual document data is transmitted - only mathematical proofs
2. Verification Process
SelfBackendVerifier validates the proof's cryptographic integrity
Checks that the proof was generated for your specific application (scope)
Verifies the proof against on-chain merkle roots (ensures document validity)
Confirms all requirements (age, nationality, etc.) are met
Returns detailed verification results
3. Configuration Management
Uses IConfigStorage to determine verification requirements
Supports different rules based on user context (via
userDefinedData
)Enables dynamic verification scenarios (e.g., different age requirements per transaction type)
Architecture Overview
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Mobile App │ │ Self Relayer │ │ Your Backend │
│ │ │ │ │ │
│ Generates ZK │───▶│ Forwards proof │───▶│ SelfBackend │
│ Proof from │ │ to your │ │ Verifier │
│ Document NFC │ │ endpoint │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ IConfigStorage │
│ │
│ Determines │
│ verification │
│ requirements │
└─────────────────┘
Constructor
new SelfBackendVerifier(
scope: string,
endpoint: string,
mockPassport: boolean,
allowedIds: Map<AttestationId, boolean>,
configStorage: IConfigStorage,
userIdentifierType: UserIdType
)
Parameters
scope
string
Your application's unique identifier. Must match the scope used in SelfAppBuilder. Max 31 characters.
endpoint
string
Your backend verification endpoint URL. Must be publicly accessible and match your frontend configuration.
mockPassport
boolean
false
for real documents (mainnet), true
for testing with mock documents (testnet)
allowedIds
Map<AttestationId, boolean>
Map of allowed document types. Key: attestation ID, Value: allowed status
configStorage
IConfigStorage
Configuration storage implementation that determines verification requirements
userIdentifierType
UserIdType
Type of user identifier: 'uuid'
or 'hex'
(for blockchain addresses)
Understanding the Parameters
Scope Matching
The scope
parameter must exactly match the scope used in your SelfAppBuilder
:
// Frontend
const selfApp = new SelfAppBuilder({
scope: "myapp-prod", // This scope...
// ... other config
});
// Backend
const verifier = new SelfBackendVerifier(
"myapp-prod", // ...must match this scope
// ... other params
);
Document Types (AttestationId)
Control which document types your application accepts:
1
- Electronic passports2
- EU ID cards
// Accept all supported document types
import { AllIds } from '@selfxyz/core';
const verifier = new SelfBackendVerifier(
// ... other params
AllIds, // Accepts all supported document types
// ... other params
);
// Accept only specific document types
const allowedIds = new Map<AttestationId, boolean>();
allowedIds.set(1, true); // Electronic passports only
const verifier = new SelfBackendVerifier(
// ... other params
allowedIds,
// ... other params
);
Network Selection
The mockPassport
parameter determines which blockchain network to use:
false
- Production: Celo Mainnet with real document datatrue
- Testing: Celo Alfajores testnet with mock document data
Configuration Storage (IConfigStorage)
The IConfigStorage
interface is crucial for dynamic verification requirements. It determines what to verify based on user context.
Built-in Storage Classes
DefaultConfigStore - Simple Static Configuration
Perfect for applications with uniform verification requirements:
import { DefaultConfigStore } from '@selfxyz/core';
const configStore = new DefaultConfigStore({
minimumAge: 21,
excludedCountries: ['IRN', 'PRK'],
ofac: true
});
// Always returns the same configuration regardless of user or context
InMemoryConfigStore - Dynamic Configuration
Enables different verification rules based on user context:
import { InMemoryConfigStore } from '@selfxyz/core';
const configStore = new InMemoryConfigStore(
async (userIdentifier: string, userDefinedData: string) => {
// Parse user-defined data to determine requirements
const context = JSON.parse(userDefinedData);
// Return different config IDs based on context
if (context.action === 'high_value_transfer') {
return 'strict_verification';
} else if (context.action === 'basic_signup') {
return 'standard_verification';
}
return 'default_verification';
}
);
// Set up different verification configurations
await configStore.setConfig('strict_verification', {
minimumAge: 21,
excludedCountries: ['IRN', 'PRK', 'CUB'],
ofac: true
});
await configStore.setConfig('standard_verification', {
minimumAge: 18,
excludedCountries: [],
ofac: false
});
Main Verification Method
verify()
This is the core method that validates zero-knowledge proofs:
async verify(
attestationId: AttestationId,
proof: VcAndDiscloseProof,
pubSignals: BigNumberish[],
userContextData: string
): Promise<VerificationResult>
Parameters
attestationId
AttestationId
Document type identifier (1 = electronic passport, 2 = EU ID card)
proof
VcAndDiscloseProof
Zero-knowledge proof object containing cryptographic proof arrays
pubSignals
BigNumberish[]
Public signals from the zero-knowledge proof
userContextData
string
Hex-encoded string containing user context and configuration data
Return Value
The method returns a VerificationResult
object with comprehensive verification details:
{
attestationId: AttestationId; // Document type that was verified
isValidDetails: {
isValid: boolean; // Overall cryptographic proof validity
isOlderThanValid: boolean; // Age requirement validation
isOfacValid: boolean; // OFAC sanctions check result
};
forbiddenCountriesList: string[]; // Countries excluded from the proof
discloseOutput: { // Disclosed document information
nullifier: string; // Unique proof identifier (prevents reuse)
forbiddenCountriesListPacked: string[];
issuingState: string; // Country that issued the document
name: string; // Full name (if disclosed)
idNumber: string; // Document number
nationality: string; // Nationality
dateOfBirth: string; // Date of birth (if disclosed)
gender: string; // Gender
expiryDate: string; // Document expiry date
olderThan: string; // Age verification result
ofac: boolean[]; // OFAC check results [passportNo, nameAndDob, nameAndYob]
};
userData: {
userIdentifier: string; // User identifier from context
userDefinedData: string; // Custom user data
};
}
Error Handling
The method throws ConfigMismatchError
when verification requirements don't match:
try {
const result = await verifier.verify(attestationId, proof, pubSignals, userContextData);
// Handle successful verification
} catch (error: any) {
if (error.name === 'ConfigMismatchError') {
console.error('Configuration mismatches:', error.issues);
// error.issues contains detailed information about what failed
} else {
console.error('Verification error:', error);
}
}
Common ConfigMismatch Types:
InvalidId
- Attestation ID not in allowedIdsInvalidScope
- Proof was generated for a different applicationInvalidRoot
- Merkle root not found on blockchainInvalidForbiddenCountriesList
- Countries don't match configurationInvalidMinimumAge
- Age requirement mismatchInvalidTimestamp
- Proof timestamp out of valid range (±1 day)InvalidOfac
- OFAC check requirements mismatchConfigNotFound
- Configuration not found in storage
Complete Implementation Example
Here's a full example showing how to integrate SelfBackendVerifier
in a real application:
Setting Up the Verifier
import {
SelfBackendVerifier,
InMemoryConfigStore,
AllIds,
AttestationId
} from '@selfxyz/core';
// Configure dynamic verification based on user context
const configStorage = new InMemoryConfigStore(
async (userIdentifier: string, userDefinedData: string) => {
const context = JSON.parse(userDefinedData);
switch (context.action) {
case 'financial_transaction':
return context.amount > 10000 ? 'high_value_kyc' : 'standard_kyc';
case 'age_verification':
return 'age_check_only';
case 'geographic_restriction':
return 'country_filter';
default:
return 'basic_verification';
}
}
);
// Set up different verification configurations
await configStorage.setConfig('high_value_kyc', {
minimumAge: 21,
excludedCountries: ['IRN', 'PRK', 'CUB'],
ofac: true
});
await configStorage.setConfig('standard_kyc', {
minimumAge: 18,
excludedCountries: ['IRN', 'PRK'],
ofac: false
});
await configStorage.setConfig('age_check_only', {
minimumAge: 18,
excludedCountries: [],
ofac: false
});
await configStorage.setConfig('country_filter', {
minimumAge: 0,
excludedCountries: ['IRN', 'PRK', 'CUB'],
ofac: false
});
// Initialize the verifier
const verifier = new SelfBackendVerifier(
'myapp-prod', // Must match frontend scope
'https://api.myapp.com/verify', // Your verification endpoint
false, // Production mode (real passports)
AllIds, // Accept all document types
configStorage, // Dynamic configuration
'uuid' // User identifier type
);
API Endpoint Implementation
import { NextRequest, NextResponse } from "next/server";
import { countries, Country3LetterCode, SelfAppDisclosureConfig } from "@selfxyz/common";
import {
countryCodes,
SelfBackendVerifier,
AllIds,
DefaultConfigStore,
VerificationConfig
} from "@selfxyz/core";
export async function POST(req: NextRequest) {
console.log("Received request");
try {
const { attestationId, proof, publicSignals, userContextData } = await req.json();
if (!proof || !publicSignals || !attestationId || !userContextData) {
return NextResponse.json({
message: "Proof, publicSignals, attestationId and userContextData are required",
}, { status: 400 });
}
const disclosures_config: VerificationConfig = {
excludedCountries: [],
ofac: false,
minimumAge: 18,
};
const configStore = new DefaultConfigStore(disclosures_config);
const selfBackendVerifier = new SelfBackendVerifier(
"self-workshop",
process.env.NEXT_PUBLIC_SELF_ENDPOINT || "",
true,
AllIds,
configStore,
"hex",
);
const result = await selfBackendVerifier.verify(
attestationId,
proof,
publicSignals,
userContextData
);
if (!result.isValidDetails.isValid) {
return NextResponse.json({
status: "error",
result: false,
message: "Verification failed",
details: result.isValidDetails,
}, { status: 500 });
}
const saveOptions = (await configStore.getConfig(
result.userData.userIdentifier
)) as unknown as SelfAppDisclosureConfig;
if (result.isValidDetails.isValid) {
console.log(result.discloseOutput);
return NextResponse.json({
status: "success",
result: result.isValidDetails.isValid,
credentialSubject: result.discloseOutput,
verificationOptions: {
minimumAge: saveOptions.minimumAge,
ofac: saveOptions.ofac,
excludedCountries: saveOptions.excludedCountries?.map(
(countryName) => {
const entry = Object.entries(countryCodes).find(
([_, name]) => name === countryName
);
return entry ? entry[0] : countryName;
}
),
},
});
} else {
return NextResponse.json({
status: "error",
result: result.isValidDetails.isValid,
message: "Verification failed",
details: result,
}, { status: 400 });
}
} catch (error) {
console.error("Error verifying proof:", error);
return NextResponse.json({
status: "error",
result: false,
message: error instanceof Error ? error.message : "Unknown error",
}, { status: 500 });
}
}
Advanced Usage Patterns
Context-Aware Verification
Use different verification rules based on user actions:
// Frontend sends context via userDefinedData
const userDefinedData = Buffer.from(JSON.stringify({
action: 'financial_transaction',
amount: 50000,
currency: 'USD',
timestamp: Date.now()
})).toString('hex').padEnd(128, '0');
// Backend uses context to determine requirements
const configStorage = new InMemoryConfigStore(
async (userIdentifier: string, userDefinedData: string) => {
const context = JSON.parse(userDefinedData);
if (context.action === 'financial_transaction') {
return context.amount > 10000 ? 'high_value_kyc' : 'standard_kyc';
}
return 'default_verification';
}
);
Nullifier Management
Prevent proof reuse by tracking nullifiers:
// In-memory storage for demo (use database in production)
const usedNullifiers = new Set<string>();
export async function POST(request: NextRequest) {
// ... verification logic ...
const result = await verifier.verify(/* ... */);
// Check if nullifier has been used before
if (usedNullifiers.has(result.discloseOutput.nullifier)) {
return NextResponse.json(
{ error: 'Proof has already been used' },
{ status: 400 }
);
}
// Store nullifier to prevent reuse
usedNullifiers.add(result.discloseOutput.nullifier);
// ... rest of response logic ...
}
Types Reference
VerificationConfig
Configuration object for verification requirements:
{
minimumAge: number; // Minimum age requirement
excludedCountries: string[]; // ISO 3-letter country codes to exclude
ofac: boolean; // Enable OFAC sanctions checking
}
VcAndDiscloseProof
Zero-knowledge proof structure:
{
a: [BigNumberish, BigNumberish];
b: [[BigNumberish, BigNumberish], [BigNumberish, BigNumberish]];
c: [BigNumberish, BigNumberish];
}
AttestationId
Document type identifiers (numeric values):
1
- Electronic passport2
- EU ID card
Best Practices
1. Configuration Management
Use appropriate storage:
DefaultConfigStore
for simple apps,InMemoryConfigStore
for dynamic requirementsValidate configurations: Ensure your verification requirements make sense for your use case
Handle edge cases: Account for missing configurations and invalid user contexts
2. Error Handling
Always catch ConfigMismatchError: This provides detailed information about why verification failed
Log errors appropriately: Help with debugging without exposing sensitive information
Provide meaningful error messages: Help users understand what went wrong
3. Security Considerations
Store nullifiers: Prevent proof reuse by tracking used nullifiers
Validate input: Always validate all parameters before calling verify()
Use HTTPS: Ensure your endpoint is secure and accessible
Rate limiting: Implement rate limiting to prevent abuse
4. Performance Optimization
Reuse verifier instances: Create the verifier once and reuse it
Cache configurations: Cache frequently used verification configurations
Optimize blockchain calls: The verifier makes blockchain calls to validate merkle roots
5. Development and Testing
Use mockPassport: true for development and testing
Test different scenarios: Test with various age requirements, countries, and document types
Validate scope matching: Ensure frontend and backend scopes match exactly
Use numeric AttestationIds: Use
1
for electronic passports,2
for EU ID cards
Migration Guide
If you're migrating from an older version that used direct configuration methods:
Old API (No longer supported):
// ❌ These methods no longer exist
verifier.setMinimumAge(18);
verifier.excludeCountries('Iran', 'North Korea');
verifier.enableNameAndDobOfacCheck();
New API:
// ✅ Configuration via IConfigStorage
const configStorage = new DefaultConfigStore({
minimumAge: 18,
excludedCountries: ['IRN', 'PRK'],
ofac: true
});
const verifier = new SelfBackendVerifier(
scope,
endpoint,
mockPassport,
allowedIds,
configStorage, // Configuration is now passed to constructor
userIdentifierType
);
Network Information
The verifier automatically connects to the appropriate blockchain network:
Mainnet (Real documents): Celo Mainnet -
https://forno.celo.org
Testnet (Mock documents): Celo Alfajores -
https://alfajores-forno.celo-testnet.org
The network selection is automatic based on the mockPassport
parameter in the constructor.
Last updated