Multi-Organization Setup¶
This guide explains how to deploy and manage multiple organizations within SYNDI. Each organization gets completely isolated infrastructure, user pools, and data storage.
Overview¶
When you deploy a new organization, the system creates:
Separate CloudFormation stack with unique name
Separate AWS infrastructure (Lambda, API Gateway, S3 buckets, CloudFront)
Separate Cognito User Pool for authentication
Complete data isolation from other organizations
Independent scaling and monitoring
Multi-Organization Isolation¶
Each organization gets:
Resource |
Isolation Level |
Example Names |
|---|---|---|
User Pools |
Complete |
|
Lambda Functions |
Complete |
|
S3 Buckets |
Complete |
|
API Gateways |
Complete |
|
CloudFront |
Complete |
Separate distributions per organization |
Benefits:
Users from org1 cannot access org2 data
Independent API rate limiting
Separate cost tracking and billing
Independent deployment cycles
Different authentication policies per org
Resource Naming Convention¶
All resources follow this consistent pattern:
CloudFormation Stack: rawscribe-{env}-{org}
Lambda Function: rawscribe-{env}-{org}-backend
API Gateway: rawscribe-{env}-{org}-api
Cognito User Pool: rawscribe-{env}-{org}-userpool
S3 Buckets: rawscribe-{service}-{env}-{org}-{accountid}
CloudFront: {distributionid}.cloudfront.net (tagged with org)
Examples for org “acme” in stage:
Stack: rawscribe-stage-acme
Lambda: rawscribe-stage-acme-backend
API: rawscribe-stage-acme-api
User Pool: rawscribe-stage-acme-userpool
Forms S3: rawscribe-forms-stage-acme-288761742376
ELN S3: rawscribe-eln-stage-acme-288761742376
Deploying a New Organization¶
Step 1: Deploy Infrastructure¶
Deploy the complete infrastructure stack for the new organization:
# First-time deployment with all resources
ENABLE_AUTH=true CREATE_BUCKETS=true \
ADMIN_USERNAME=admin@neworg.com \
ADMIN_PASSWORD=SecurePassword2025! \
ORG=neworg ENV=stage make rs-deploy
Deployment time: 5-7 minutes
What gets created:
CloudFormation stack
Lambda function with dependency layer
API Gateway with proxy integration
Cognito User Pool with app client
Cognito Groups: ADMINS, LAB_MANAGERS, RESEARCHERS, CLINICIANS
S3 buckets (5 buckets):
Frontend hosting
Lambda configs
Forms/SOPs
ELN submissions
ELN drafts
CloudFront distribution
IAM roles and policies
Admin user (if credentials provided)
Step 2: Sync Configuration¶
After deployment, sync configuration files from CloudFormation outputs:
make sync-configs ENV=stage ORG=neworg
This updates:
infra/.config/webapp/stage-neworg.jsonwith API endpoint and Cognito IDsinfra/.config/lambda/stage-neworg.jsonwith Cognito IDs
Output:
🔍 Fetching outputs from stack: rawscribe-stage-neworg
📋 CloudFormation Outputs:
ApiEndpoint: https://abc123.execute-api.us-east-1.amazonaws.com/stage
CognitoUserPoolId: us-east-1_ABC123
CognitoClientId: abc123def456
CloudFrontURL: https://d1234.cloudfront.net
📝 Updating configuration files...
✅ Updated org-specific config: infra/.config/webapp/stage-neworg.json
✅ Updated org-specific lambda config: infra/.config/lambda/stage-neworg.json
✅ Configuration sync complete!
Step 3: Customize Organization Settings (Optional)¶
Edit organization-specific configurations:
# Edit webapp config for branding
vi infra/.config/webapp/stage-neworg.json
Add organization-specific settings:
{
"webapp": {
"branding": {
"title": "SYNDI - New Organization",
"org_name": "New Organization Labs"
},
"ui": {
"theme": "light",
"logo": "/assets/neworg-logo.png"
}
}
}
# Edit lambda config for custom settings
vi infra/.config/lambda/stage-neworg.json
Add organization-specific settings:
{
"lambda": {
"email_settings": {
"from_email": "noreply@neworg.com",
"support_email": "support@neworg.com"
},
"file_uploads": {
"max_file_size_mb": 50
},
"cors": {
"allowedOrigins": [
"https://syndi.neworg.com",
"http://localhost:3000"
]
}
}
}
Step 4: Redeploy with Custom Configs¶
If you customized configs, redeploy:
ORG=neworg ENV=stage make rs-deploy-only
Step 5: Upload SOPs¶
Upload Standard Operating Procedures to the forms bucket:
# Get AWS account ID
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# Upload SOP file
aws s3 cp your-sop.yaml \
s3://rawscribe-forms-stage-neworg-${ACCOUNT_ID}/sops/
# Or sync entire directory
aws s3 sync ./sops-directory \
s3://rawscribe-forms-stage-neworg-${ACCOUNT_ID}/sops/
Step 6: Test Deployment¶
Verify the deployment works correctly:
# Check deployment status
ORG=neworg ENV=stage make check-rs
# Test authentication (if admin user created)
ORG=neworg ENV=stage make test-jwt-aws
# Test API endpoints
API_ENDPOINT=$(aws cloudformation describe-stacks \
--stack-name rawscribe-stage-neworg \
--query 'Stacks[0].Outputs[?OutputKey==`ApiEndpoint`].OutputValue' \
--output text)
curl ${API_ENDPOINT}/health
# Expected: {"status":"healthy",...}
Managing Multiple Organizations¶
Deployment Best Practices¶
Initial Infrastructure Setup (ONCE per org)¶
# Create all resources including buckets
ENABLE_AUTH=true CREATE_BUCKETS=true \
ADMIN_USERNAME=admin@org.com \
ADMIN_PASSWORD=SecurePass! \
ORG=myorg ENV=stage make rs-deploy
# Sync configs
make sync-configs ENV=stage ORG=myorg
# Commit org-specific configs
git add infra/.config/webapp/stage-myorg.json
git add infra/.config/lambda/stage-myorg.json
git commit -m "Add stage-myorg configs with deployed resource IDs"
Regular Code Updates (FREQUENT)¶
# Fast Lambda-only update - use this 95% of the time
ORG=myorg ENV=stage make rs-deploy-function
# OR full stack update if infrastructure changed
ENABLE_AUTH=true CREATE_BUCKETS=false \
ORG=myorg ENV=stage make rs-deploy
# Sync configs only if API endpoint changed
make sync-configs ENV=stage ORG=myorg
Parallel Deployments¶
Different organizations can deploy simultaneously:
# Terminal 1: Deploy org1
ORG=org1 ENV=stage make rs-deploy &
# Terminal 2: Deploy org2 (parallel)
ORG=org2 ENV=stage make rs-deploy &
# Each uses its own build directory:
# .aws-sam-stage-org1/
# .aws-sam-stage-org2/
Benefits:
No build conflicts
Faster overall deployment
Independent failure handling
Configuration Management¶
Organization-Specific Overrides¶
infra/.config/lambda/stage-org1.json - Org1-specific settings:
{
"lambda": {
"file_uploads": {
"max_file_size_mb": 50
},
"email_settings": {
"from_email": "noreply@org1.com"
}
}
}
infra/.config/lambda/stage-org2.json - Org2-specific settings:
{
"lambda": {
"file_uploads": {
"max_file_size_mb": 100
},
"email_settings": {
"from_email": "noreply@org2.com"
}
}
}
Merge behavior: Org-specific settings override base settings via deep merge.
Viewing All Organizations¶
# Check all organizations
make check-rs
# Output shows all deployed orgs:
=== org1 Resources (stage) ===
Lambda: rawscribe-stage-org1-backend
API Gateway: rawscribe-stage-org1-api
API Endpoint: https://abc123.execute-api.us-east-1.amazonaws.com/stage/
...
=== org2 Resources (stage) ===
Lambda: rawscribe-stage-org2-backend
API Gateway: rawscribe-stage-org2-api
API Endpoint: https://def456.execute-api.us-east-1.amazonaws.com/stage/
...
User Management Per Organization¶
Each organization has its own Cognito User Pool with separate users.
Creating Users¶
# Get User Pool ID for organization
USER_POOL_ID=$(aws cloudformation describe-stacks \
--stack-name rawscribe-stage-org1 \
--query 'Stacks[0].Outputs[?OutputKey==`CognitoUserPoolId`].OutputValue' \
--output text)
# Create user
aws cognito-idp admin-create-user \
--user-pool-id ${USER_POOL_ID} \
--username researcher@org1.com \
--user-attributes Name=email,Value=researcher@org1.com \
--temporary-password TempPass123! \
--message-action SUPPRESS
# Add to RESEARCHERS group
aws cognito-idp admin-add-user-to-group \
--user-pool-id ${USER_POOL_ID} \
--username researcher@org1.com \
--group-name RESEARCHERS
# Set permanent password
aws cognito-idp admin-set-user-password \
--user-pool-id ${USER_POOL_ID} \
--username researcher@org1.com \
--password ResearcherPass123! \
--permanent
Using Makefile Helper¶
# Create user with Makefile (if helper exists)
make create-rs-user ENV=stage ORG=org1 \
USERNAME=researcher@org1.com \
PASSWORD=ResearcherPass! \
GROUP=RESEARCHERS
Cost Management¶
Resource Tagging¶
All resources are automatically tagged with:
Environment: stage
Organization: org1
Application: SYNDI
Component: Backend-API (or Frontend, Storage, etc.)
Cost Tracking¶
Track costs per organization using AWS Cost Explorer:
# View costs by organization tag
aws ce get-cost-and-usage \
--time-period Start=2025-01-01,End=2025-01-31 \
--granularity MONTHLY \
--metrics "BlendedCost" \
--group-by Type=TAG,Key=Organization
Cost Optimization¶
Per Organization:
Monitor Lambda invocations and adjust memory/timeout
Review S3 storage growth and implement lifecycle policies
Optimize CloudFront cache settings
Set up budget alerts per organization tag
Example Lifecycle Policy:
# Move old ELN submissions to Glacier after 90 days
aws s3api put-bucket-lifecycle-configuration \
--bucket rawscribe-eln-stage-org1-${ACCOUNT_ID} \
--lifecycle-configuration file://lifecycle.json
Troubleshooting¶
Deployment Issues¶
Stack Name Conflicts:
Error: Stack rawscribe-stage-org1 already exists
Solution: Use different ORG parameter or check existing stack:
aws cloudformation describe-stacks --stack-name rawscribe-stage-org1
S3 Bucket Naming Issues:
Error: Bucket already exists
Solution:
Use
CREATE_BUCKETS=falseif buckets existOr choose different ORG name
Cognito User Pool Limits:
Error: LimitExceededException
Solution: Check AWS limits (default 1000 pools per region). Request increase or use existing pools.
Verification Commands¶
# Check stack status
ORG=org1 ENV=stage make check-rs-stack-status
# List all stacks for organization
aws cloudformation list-stacks \
--query 'StackSummaries[?contains(StackName,`org1`)]'
# Check Lambda function
aws lambda get-function \
--function-name rawscribe-stage-org1-backend
# Check S3 buckets
aws s3 ls | grep "rawscribe.*org1"
# Show all S3 buckets for organization
ORG=org1 ENV=stage make show-rs-s3-buckets
Cross-Organization Issues¶
Users can’t access other org’s data:
This is expected - organizations are isolated
Users need separate accounts in each org’s User Pool
Shared resources:
Only the SAM deployment bucket is shared:
rawscribe-sam-deployments-{accountid}All other resources are org-specific
Environment Teardown¶
Safe Teardown (Preserves Data)¶
Removes Lambda and API Gateway, keeps Cognito and S3:
# WARNING: This will delete the Lambda and API Gateway
ORG=org1 ENV=stage make rs-teardown
# Buckets and User Pool are preserved
# Redeploy with: ORG=org1 ENV=stage make rs-deploy
Complete Teardown (DANGEROUS - Destroys User Data!)¶
Only for dev/test environments or complete rebuilds:
# WARNING: Deletes Cognito users, S3 data, everything!
aws cloudformation delete-stack \
--stack-name rawscribe-stage-org1 \
--region us-east-1
# Wait for deletion
aws cloudformation wait stack-delete-complete \
--stack-name rawscribe-stage-org1
# Manually delete S3 buckets (CloudFormation can't delete non-empty buckets)
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws s3 rm s3://rawscribe-forms-stage-org1-${ACCOUNT_ID} --recursive
aws s3 rb s3://rawscribe-forms-stage-org1-${ACCOUNT_ID}
# Repeat for other buckets...
Security Considerations¶
Organization Isolation¶
Network: Each org has separate API Gateway endpoints
Authentication: Separate User Pools mean separate user databases
Authorization: JWT tokens from org1 won’t work with org2 Lambda
Data: S3 bucket policies restrict access to specific Lambda functions
Monitoring: CloudWatch logs are separated by function name
IAM Roles¶
Each organization’s Lambda has a unique execution role:
rawscribe-stage-org1-lambda-role
rawscribe-stage-org2-lambda-role
Roles grant access only to that org’s S3 buckets.
API Gateway Security¶
CORS configured per organization
Rate limiting applied per API Gateway
Different throttling settings possible per org
Best Practices¶
Use Consistent Naming: Stick to lowercase, alphanumeric org names (no hyphens or special characters)
Tag Resources: Use Organization tag for cost tracking and resource management
Document Org-Specific Configs: Add comments explaining custom settings
Monitor Per Organization: Set up CloudWatch alarms per org
Backup Data: Configure S3 versioning and cross-region replication for critical orgs
Regular Audits: Review user access and permissions quarterly per org
Independent Testing: Test each org’s deployment separately