Configuration System Architecture¶
This document explains SYNDI’s configuration architecture, including how configuration files are structured, merged, and deployed across different environments and organizations.
Overview¶
SYNDI uses a three-tier configuration system designed to separate infrastructure values from application behavior while supporting multiple organizations:
CloudFormation Outputs → Lambda Environment Variables (infrastructure values)
Base JSON Configs → Application behavior settings
Org-specific Override Configs → Organization customizations
This architecture follows these principles:
Infrastructure values (Cognito IDs, bucket names, API endpoints) → CloudFormation outputs → Lambda environment variables
Application behavior (file limits, retry logic, UI preferences) → JSON config files
Deployment parameters (ENV, ORG, ENABLE_AUTH, CREATE_BUCKETS) → Makefile command line
Zero redundancy - Each value defined exactly once
Pure functional deployment -
f(ENV, ORG, parameters) → Infrastructure
Configuration File Structure¶
Directory Layout¶
infra/.config/ # Central config management (NOT in git)
├── lambda/
│ ├── dev.json # Base Lambda config for dev
│ ├── dev-{org}.json # Org-specific Lambda overrides for dev
│ ├── test.json # Base Lambda config for test
│ ├── stage.json # Base Lambda config for stage
│ ├── stage-{org}.json # Org-specific Lambda overrides for stage
│ ├── prod.json # Base Lambda config for prod
│ └── prod-{org}.json # Org-specific Lambda overrides for prod
├── webapp/
│ ├── dev.json # Base webapp config for dev
│ ├── dev-{org}.json # Org-specific webapp overrides for dev
│ ├── test.json # Base webapp config for test
│ ├── stage.json # Base webapp config for stage
│ ├── stage-{org}.json # Org-specific webapp overrides for stage
│ ├── prod.json # Base webapp config for prod
│ └── prod-{org}.json # Org-specific webapp overrides for prod
└── cloudformation/ # CloudFormation parameter files
├── dev.json
├── stage.json
└── prod.json
What Goes Where¶
✅ JSON Config Files (Application Behavior)¶
Store these in infra/.config/lambda/{env}.json or infra/.config/webapp/{env}.json:
File upload limits (
max_file_size_mb)Retry policies (
max_retries,backoff_multiplier)Email settings (
from_email,support_email)UI preferences (
theme,showStatus)Feature flags (
autosave.enabled)CORS allowed origins
Service account configurations
❌ NOT in JSON Config Files (Infrastructure Values)¶
These come from CloudFormation outputs or environment variables:
S3 bucket names
Cognito User Pool IDs
Cognito Client IDs
API Gateway endpoints
Lambda function names
Any CloudFormation outputs
Configuration Merge Process¶
1. Base + Org-specific Merge¶
The config-merger.py script performs deep merging:
# Merge process (automatic via Makefile)
python infra/scripts/config-merger.py \
"infra/.config/lambda/stage.json" \ # Base config
"infra/.config/lambda/stage-uga.json" \ # Org-specific overrides
"backend/rawscribe/.config/config.json" # Output (merged)
Deep Merge Strategy:
Org-specific values override base values
Nested objects are merged recursively
Arrays are replaced (not merged)
Null values in org config remove base values
Example:
Base config (stage.json):
{
"lambda": {
"auth": {
"provider": "cognito",
"required": true
},
"file_uploads": {
"max_file_size_mb": 25
}
}
}
Org-specific config (stage-uga.json):
{
"lambda": {
"file_uploads": {
"max_file_size_mb": 50
},
"email_settings": {
"from_email": "noreply@uga.edu"
}
}
}
Merged result:
{
"lambda": {
"auth": {
"provider": "cognito",
"required": true
},
"file_uploads": {
"max_file_size_mb": 50
},
"email_settings": {
"from_email": "noreply@uga.edu"
}
}
}
2. CloudFormation Output Sync¶
After deployment, sync-configs-from-cloudformation.py updates org-specific configs with infrastructure values:
# Sync configs from deployed stack
make sync-configs ENV=stage ORG=uga
This script:
Queries CloudFormation stack outputs
Extracts API endpoint, Cognito User Pool ID, and Client ID
Deep-merges these values into
infra/.config/webapp/stage-uga.jsonPreserves all existing custom fields
Updates only infrastructure-related values
What gets synced:
webapp.apiEndpoint← CloudFormationApiEndpointoutputwebapp.auth.cognito.userPoolId← CloudFormationCognitoUserPoolIdoutputwebapp.auth.cognito.clientId← CloudFormationCognitoClientIdoutputwebapp.api.proxyTarget← CloudFormationApiEndpointoutput
3. Runtime Configuration Loading¶
Lambda Backend¶
The Lambda function receives configuration from two sources:
Environment Variables (from CloudFormation):
Environment:
Variables:
ENV: stage
ORG: uga
CONFIG_S3_BUCKET: rawscribe-lambda-stage-uga-123456789
CONFIG_S3_KEY: config.json
COGNITO_REGION: us-east-1
COGNITO_USER_POOL_ID: us-east-1_ABC123
COGNITO_CLIENT_ID: abc123def456
FORMS_BUCKET: rawscribe-forms-stage-uga-123456789
ELN_BUCKET: rawscribe-eln-stage-uga-123456789
DRAFTS_BUCKET: rawscribe-eln-drafts-stage-uga-123456789
Config File (uploaded to S3): The merged JSON config is uploaded to S3 during deployment:
# Automatic via rs-deploy-only
aws s3 cp infra/.config/lambda/stage-uga.json \
s3://rawscribe-lambda-stage-uga-123456789/config.json
Auth Provider System:
The backend uses pluggable authentication providers with environment-aware configuration:
# backend/rawscribe/utils/auth_providers/factory.py
provider = AuthProviderFactory.create(config) # Returns CognitoProvider or JWTProvider
# backend/rawscribe/utils/auth_providers/cognito_provider.py
# Environment variables take precedence (CloudFormation truth)
def get_user_pool_id(self) -> str:
return os.environ.get('COGNITO_USER_POOL_ID') or \
self._cognito_config.get('userPoolId')
Runtime Config Endpoint:
Clients can query /api/config/runtime to get actual deployed configuration:
curl https://api.example.com/api/config/runtime
Returns:
{
"auth": {
"provider": "cognito",
"config": {
"userPoolId": "us-east-1_ABC123",
"clientId": "abc123def456",
"region": "us-east-1",
"source": "environment"
}
}
}
The source field indicates whether config came from environment variables (CloudFormation) or config file (baked into Lambda).
Frontend Webapp¶
Configuration is built into the static assets:
# Build process merges configs
python infra/scripts/config-merger.py \
"infra/.config/webapp/stage.json" \
"infra/.config/webapp/stage-uga.json" \
"frontend/public/config.json"
# Frontend build includes config.json
npm run build # config.json → frontend/dist/config.json
Frontend loads config at runtime:
// Fetch config based on environment
const config = await fetch('/config.json').then(r => r.json());
Configuration Workflow¶
Initial Deployment (New Organization)¶
# 1. Deploy infrastructure with bucket creation
ENABLE_AUTH=true CREATE_BUCKETS=true \
ORG=neworg ENV=stage make rs-deploy
# 2. Sync configs from CloudFormation outputs
make sync-configs ENV=stage ORG=neworg
# 3. Review and customize org-specific config
vi infra/.config/webapp/stage-neworg.json
vi infra/.config/lambda/stage-neworg.json
# 4. Redeploy with updated configs
ORG=neworg ENV=stage make rs-deploy-only
Updating Application Settings¶
# 1. Edit base or org-specific config
vi infra/.config/lambda/stage-uga.json
# 2. Deploy configuration changes
ORG=uga ENV=stage make rs-deploy-only
Updating Infrastructure¶
When CloudFormation outputs change (e.g., new Cognito pool):
# 1. Deploy infrastructure changes
ORG=uga ENV=stage make rs-deploy
# 2. Sync updated values to configs
make sync-configs ENV=stage ORG=uga
# 3. Verify changes
git diff infra/.config/webapp/stage-uga.json
Configuration Examples¶
Minimal Lambda Config (Base)¶
infra/.config/lambda/stage.json:
{
"lambda": {
"auth": {
"provider": "cognito",
"required": true,
"cognito": {
"region": "us-east-1"
}
},
"file_uploads": {
"max_file_size_mb": 25,
"allowed_extensions": [".pdf", ".doc", ".txt", ".xls", ".xlsx"],
"temp_storage_retention_days": 7
},
"retry": {
"max_retries": 3,
"backoff_multiplier": 2
},
"cors": {
"allowedOrigins": [
"http://localhost:3000",
"http://localhost:5173"
]
}
}
}
Organization-Specific Lambda Override¶
infra/.config/lambda/stage-uga.json:
{
"lambda": {
"email_settings": {
"from_email": "noreply@uga.edu",
"support_email": "support@uga.edu"
},
"file_uploads": {
"max_file_size_mb": 50
},
"cors": {
"allowedOrigins": [
"https://syndi.uga.edu",
"http://localhost:3000"
]
}
}
}
Minimal Webapp Config (Base)¶
infra/.config/webapp/stage.json:
{
"webapp": {
"apiEndpoint": "TO_BE_FILLED_BY_SYNC_CONFIGS",
"auth": {
"required": true,
"provider": "cognito",
"cognito": {
"region": "us-east-1",
"userPoolId": "TO_BE_FILLED_BY_SYNC_CONFIGS",
"clientId": "TO_BE_FILLED_BY_SYNC_CONFIGS"
},
"session": {
"timeout": 3600000,
"refreshBuffer": 300000
}
}
}
}
Organization-Specific Webapp Override¶
infra/.config/webapp/stage-uga.json:
{
"webapp": {
"branding": {
"title": "SYNDI - University of Georgia",
"org_name": "UGA Research Labs"
},
"ui": {
"theme": "light",
"logo": "/assets/uga-logo.png"
}
}
}
Note: Infrastructure values (apiEndpoint, userPoolId, clientId) are automatically filled by sync-configs.
Configuration Precedence¶
Configuration values are resolved in this order (highest to lowest priority):
Lambda Environment Variables (from CloudFormation)
Org-specific JSON Config (
{env}-{org}.json)Base JSON Config (
{env}.json)Application Defaults (hardcoded in code)
Example resolution for COGNITO_USER_POOL_ID:
1. Check Lambda env var: COGNITO_USER_POOL_ID
2. Check org config: lambda.auth.cognito.userPoolId (from stage-uga.json)
3. Check base config: lambda.auth.cognito.userPoolId (from stage.json)
4. Use default: None (authentication disabled)
Troubleshooting¶
Config Merge Issues¶
Problem: Org-specific config not taking effect
Solution:
# Verify merge is happening
make config ENV=stage ORG=uga
# Check merged output
cat backend/rawscribe/.config/config.json | jq '.lambda.file_uploads'
CloudFormation Sync Issues¶
Problem: sync-configs not updating values
Solution:
# Verify stack exists and has outputs
aws cloudformation describe-stacks \
--stack-name rawscribe-stage-uga \
--query 'Stacks[0].Outputs'
# Run sync with debug output
python -u infra/scripts/sync-configs-from-cloudformation.py \
--env stage --org uga
Missing Configuration¶
Problem: Lambda can’t find config values
Solution:
# Check environment variables
aws lambda get-function-configuration \
--function-name rawscribe-stage-uga-backend \
--query 'Environment.Variables'
# Check S3 config upload
aws s3 cp s3://rawscribe-lambda-stage-uga-123456789/config.json - | jq
Best Practices¶
Use Base Configs for Shared Settings: Put common settings in base configs (
stage.json)Use Org Configs for Customizations: Put organization-specific values in org configs (
stage-uga.json)Never Hardcode Infrastructure Values: Always use CloudFormation outputs or environment variables
Run sync-configs After Deployment: Always sync configs after deploying infrastructure changes
Version Control Base Configs: Base configs can be committed (no sensitive data)
Keep Org Configs Private: Org-specific configs may contain sensitive information
Validate JSON: Use
jqto validate JSON before deploymentDocument Custom Fields: Add comments (via commit messages) explaining custom config fields