This document provides a comprehensive overview of the configuration system used in the Open Cap Table Protocol (OCP) repository.
OCP uses an environment-based configuration approach that combines:
- Environment-specific .env files (.env.local, .env.dev, .env.prod)
- Runtime environment variables
- Blockchain network configurations
- The
setupEnv()function insrc/utils/env.jsis called at application startup - If running in a Docker environment (DOCKER_ENV=true), the function uses runtime environment variables
- Otherwise, it loads environment variables from the .env file specified by the USE_ENV_FILE environment variable (defaults to .env)
- The environment file is located by searching upwards from the current working directory
- Once loaded, the environment variables are accessible via
process.env
The repository uses different environment files for different deployment environments:
| File | Purpose |
|---|---|
.env.local |
Local development environment settings |
.env.dev |
Development/testnet environment settings |
.env.prod |
Production/mainnet environment settings |
MongoDB connection settings are configured through the DATABASE_URL environment variable:
# Offchain db connection string for mongodb
DATABASE_URL="mongodb://ocp:ocp@localhost:27017/mongo?authSource=admin&retryWrites=true&w=majority"
DATABASE_REPLSET="0" # set to "1" if using --replSet option in mongo
DATABASE_OVERRIDE="" # optional override for database nameThe database connection is established in src/db/config/mongoose.ts using these parameters.
The system supports multiple blockchain networks, configured through the following settings:
# RPC url for the network
RPC_URL=http://127.0.0.1:8545
# Chain ID of the target network
CHAIN_ID=31337
# Private key for contract deployment and transactions
PRIVATE_KEY=your_private_key_hereSupported networks are defined in src/utils/chains.js:
export const SUPPORTED_CHAINS = {
8453: {
// Base Mainnet
name: "Base Mainnet",
rpcUrl: process.env.BASE_RPC_URL,
wsUrl: (process.env.BASE_RPC_URL || "").replace("https://", "wss://"),
},
84532: {
// Base Sepolia
name: "Base Sepolia",
rpcUrl: process.env.BASE_SEPOLIA_RPC_URL,
wsUrl: (process.env.BASE_SEPOLIA_RPC_URL || "").replace("https://", "wss://"),
},
31337: {
// Anvil
name: "Anvil",
rpcUrl: "http://localhost:8545",
wsUrl: "ws://localhost:8545",
},
};The system is designed to support multiple blockchain networks simultaneously. Each chain requires specific configuration:
# Base Mainnet RPC
BASE_RPC_URL=https://mainnet.base.org
# Base Sepolia Testnet RPC
BASE_SEPOLIA_RPC_URL=https://sepolia.base.orgChain-specific middleware is implemented in src/app.js to handle routing requests to the appropriate blockchain network:
const chainMiddleware = (req, res, next) => {
const chainId = req.body.chain_id;
if (!chainId) {
return res.status(400).send("chain_id is required for issuer creation");
}
const chainConfig = getChainConfig(Number(chainId));
if (!chainConfig) {
return res.status(400).send(`Unsupported chain ID: ${chainId}`);
}
req.chain = Number(chainId);
next();
};The system includes a real-time event listener that monitors contract events across multiple blockchain networks. Configuration for the websocket system includes:
# Optional WebSocket specific configuration
WS_RECONNECT_INTERVAL=5000 # Reconnection interval in milliseconds
WS_MAX_RECONNECT_ATTEMPTS=10 # Maximum reconnection attempts
EVENT_FILTER_FROM_BLOCK=0 # Start listening from this block (0 = from deployment)The websocket listener is configured in src/utils/websocket.ts and automatically groups contracts by chain ID to optimize connection management:
// Group contracts by chain ID
const contractsByChain = contracts.reduce((acc, { address, chain_id }) => {
if (!acc[chain_id]) acc[chain_id] = [];
acc[chain_id].push(address);
return acc;
}, {});To improve performance, the system implements a contract instance caching mechanism:
# Contract cache configuration
CONTRACT_CACHE_TTL=3600 # Cache time-to-live in seconds (0 = no expiration)
MAX_CACHED_CONTRACTS=100 # Maximum number of cached contract instancesThe contract cache is implemented in src/app.js using a middleware function:
const contractMiddleware = async (req, res, next) => {
if (!req.body.issuerId) {
return res.status(400).send("issuerId is required");
}
const issuer = await readIssuerById(req.body.issuerId);
if (!issuer) return res.status(404).send("issuer not found");
const cacheKey = `${issuer.chain_id}-${req.body.issuerId}`;
if (!contractCache[cacheKey]) {
const contract = await getContractInstance(issuer.deployed_to, issuer.chain_id);
contractCache[cacheKey] = { contract };
}
req.contract = contractCache[cacheKey].contract;
next();
};Contract addresses are stored in environment variables and updated during deployment:
# Factory and reference implementation addresses
FACTORY_ADDRESS=0x...
REFERENCE_DIAMOND=0x...
# Facet addresses for the diamond contract
DIAMOND_CUT_FACET=0x...
ISSUER_FACET=0x...
STAKEHOLDER_FACET=0x...
STOCK_CLASS_FACET=0x...
# ... other facetsThese addresses are used when creating contract instances in src/chain-operations/getContractInstances.js.
Server settings for the Express application:
# Server port
PORT=8293
# Optional Sentry error reporting
SENTRY_DSN=your_sentry_dsnThe system supports different deployment environments through the deploy_factory.sh script:
# Deploy to local environment
yarn deploy:local
# Deploy to testnet
yarn deploy:testnet
# Deploy to mainnet
yarn deploy:mainnetEach deployment command loads the appropriate environment file:
deploy:localuses.env.localdeploy:testnetuses.env.devdeploy:mainnetuses.env.prod
The deployment script includes confirmation prompts and address verification for non-local environments to prevent accidental deployments.
When adding new configuration values:
- Add the variable to all environment files (
.env.local,.env.dev,.env.prod) - Access the variable in code via
process.env.YOUR_VARIABLE - For sensitive values (like private keys), ensure they are not committed to source control
Key differences between environments:
-
Local (.env.local)
- Uses Anvil local blockchain (chain ID 31337)
- MongoDB running in local Docker container
- Default ports and simple configuration
-
Development (.env.dev)
- Uses testnet (like Base Sepolia)
- May use different MongoDB instance
- Contains test accounts and settings
-
Production (.env.prod)
- Uses mainnet (like Base Mainnet)
- Production MongoDB instance
- Real account credentials and settings
- May include additional security measures
import { setupEnv } from "./utils/env.js";
// Load environment variables at application startup
setupEnv();
// Access configuration values
const port = process.env.PORT || 8080;
const databaseUrl = process.env.DATABASE_URL;import { ethers } from "ethers";
import { getChainConfig } from "./utils/chains.js";
// Get chain configuration
const chainId = Number(process.env.CHAIN_ID);
const chainConfig = getChainConfig(chainId);
// Create provider
const provider = new ethers.JsonRpcProvider(chainConfig.rpcUrl);
// Use the provider for contract interactions
// ...// Import the necessary modules
import { getChainConfig } from "./utils/chains.js";
// Create a map to store providers by chain ID
const providers = new Map();
// Function to get or create provider for a chain
const getChainProvider = (chainId) => {
if (!providers.has(chainId)) {
const config = getChainConfig(chainId);
providers.set(chainId, new ethers.JsonRpcProvider(config.rpcUrl));
}
return providers.get(chainId);
};
// Use different providers based on chain ID
const provider1 = getChainProvider("8453"); // Base Mainnet
const provider2 = getChainProvider("84532"); // Base Sepoliaimport { startListener } from "./utils/websocket.ts";
// On server startup, set up listeners for all contracts
app.listen(PORT, async () => {
console.log(`Server successfully launched at:${PORT}`);
// Get all contracts that need to be monitored
const contractsToWatch = issuers
.filter(issuer => issuer?.deployed_to && issuer?.chain_id)
.map(issuer => ({
id: issuer.id,
address: issuer.deployed_to,
chain_id: issuer.chain_id
}));
// Start the websocket listeners, grouped by chain
await startListener(contractsToWatch);
});// Get contract address from environment
const factoryAddress = process.env.FACTORY_ADDRESS;
const diamondAddress = process.env.REFERENCE_DIAMOND;
// Use addresses to create contract instances
// ...- Never commit sensitive data: Private keys, API keys, and other secrets should never be committed to source control
- Use environment-specific configurations: Different settings for local, development, and production environments
- Validate configuration: Ensure required configuration is present before starting the application
- Use descriptive names: Configuration variables should have clear, descriptive names
- Document changes: Update this document when making changes to the configuration system
- Group chain-specific configuration: Keep all configuration for a specific chain together
- Implement connection pooling: For production, use connection pooling for blockchain providers
- Cache contract instances: Use the contract caching middleware to improve performance
- Monitor event listener health: Implement health checks for websocket connections