When using external services (AWS S3, OpenAI, etc.) in Sonamu projects, API keys are managed as environment variables. This guide explains how to securely store them without hardcoding in your code.
API keys should never be committed to Git! If leaked, immediately invalidate and reissue the keys.
Common API Keys
These are frequently used external service API keys in Sonamu.
AWS (S3 Storage)
# AWS Credentials
AWS_ACCESS_KEY_ID = your-aws-access-key-id
AWS_SECRET_ACCESS_KEY = your-aws-secret-access-key
# S3 Configuration
S3_REGION = ap-northeast-2
S3_BUCKET = my-project-uploads
sonamu.config.ts:
import { defineConfig } from "sonamu" ;
import { drivers } from "sonamu/storage" ;
export default defineConfig ({
server: {
storage: {
default: process . env . DRIVE_DISK ?? "fs" ,
drivers: {
s3: drivers . s3 ({
credentials: {
accessKeyId: process . env . AWS_ACCESS_KEY_ID ?? "" ,
secretAccessKey: process . env . AWS_SECRET_ACCESS_KEY ?? "" ,
},
region: process . env . S3_REGION ?? "ap-northeast-2" ,
bucket: process . env . S3_BUCKET ?? "default-bucket" ,
visibility: "private" ,
}),
},
},
} ,
}) ;
OpenAI
# OpenAI API Key
OPENAI_API_KEY = your-openai-api-key
OPENAI_ORG_ID = your-openai-org-id # Organization ID (optional)
Usage example:
import OpenAI from "openai" ;
const openai = new OpenAI ({
apiKey: process . env . OPENAI_API_KEY ,
organization: process . env . OPENAI_ORG_ID ,
});
const completion = await openai . chat . completions . create ({
model: "gpt-4" ,
messages: [{ role: "user" , content: "Hello!" }],
});
Optional External Service Integrations
Sonamu can integrate with various external services. Below are environment variable examples for commonly used services.
These services are not part of Sonamu’s default configuration. Add them only when needed for your project.
Stripe
SendGrid
Twilio
Firebase
# Stripe Payments
STRIPE_SECRET_KEY = your-stripe-secret-key
STRIPE_PUBLISHABLE_KEY = your-stripe-publishable-key
STRIPE_WEBHOOK_SECRET = your-stripe-webhook-secret
# SendGrid Email
SENDGRID_API_KEY = your-sendgrid-api-key
SENDGRID_FROM_EMAIL = noreply@example.com
# Twilio SMS
TWILIO_ACCOUNT_SID = your-twilio-account-sid
TWILIO_AUTH_TOKEN = your-twilio-auth-token
TWILIO_PHONE_NUMBER = +1234567890
# Firebase
FIREBASE_API_KEY = your-firebase-api-key
FIREBASE_PROJECT_ID = my-project
FIREBASE_APP_ID = your-firebase-app-id
Environment-Specific Key Management
Never use production keys in development environments! Use separate keys for each environment.
Development
Staging
Production
# Test or sandbox keys
OPENAI_API_KEY = your-test-openai-key
AWS_ACCESS_KEY_ID = your-test-aws-key-id
AWS_SECRET_ACCESS_KEY = your-test-aws-secret-key
S3_BUCKET = myproject-dev
STRIPE_SECRET_KEY = your-test-stripe-key
Characteristics:
Uses test mode keys
No or minimal charges
Limited permissions
# Staging-specific keys
OPENAI_API_KEY = your-staging-openai-key
AWS_ACCESS_KEY_ID = your-staging-aws-key-id
AWS_SECRET_ACCESS_KEY = your-staging-aws-secret-key
S3_BUCKET = myproject-staging
STRIPE_SECRET_KEY = your-test-stripe-key # Still test mode
Characteristics:
Separate resources from production
May incur actual charges
Same permissions as production
# Production keys (manage very strictly)
OPENAI_API_KEY = your-production-openai-key
AWS_ACCESS_KEY_ID = your-production-aws-key-id
AWS_SECRET_ACCESS_KEY = your-production-aws-secret-key
S3_BUCKET = myproject-production
STRIPE_SECRET_KEY = your-live-stripe-key # Live mode
Characteristics:
Highest level of security
Apply principle of least privilege
Periodic rotation
Creating AWS IAM User
Create User
Users → Add users
User name: sonamu-s3-user
Access type: Select Programmatic access
Set Permissions
Option 1: Attach Existing Policy AmazonS3FullAccess ❌ (too broad)
Option 2: Custom Policy (recommended) {
"Version" : "2012-10-17" ,
"Statement" : [
{
"Effect" : "Allow" ,
"Action" : [
"s3:PutObject" ,
"s3:GetObject" ,
"s3:DeleteObject" ,
"s3:ListBucket"
],
"Resource" : [
"arn:aws:s3:::myproject-uploads/*" ,
"arn:aws:s3:::myproject-uploads"
]
}
]
}
Enhance security by allowing only specific buckets and actions.
Issue Access Keys
After user creation completes:
Access key ID: your-aws-access-key-id
Secret access key: your-aws-secret-access-key
The secret access key is displayed only once! Save it to a secure location immediately.
Add to .env File
AWS_ACCESS_KEY_ID = your-aws-access-key-id
AWS_SECRET_ACCESS_KEY = your-aws-secret-access-key
Issuing OpenAI API Key
Create API Key
API keys → Create new secret key
Name: sonamu-production
Permissions: All or Restricted (recommended)
Set Usage Limits
Settings → Billing → Set Usage limits Hard limit: $100/month (automatically blocked when exceeded)
Soft limit: $80/month (notification)
Unexpected charges may occur if you don’t set usage limits!
Save Key
OPENAI_API_KEY = your-openai-api-key
Save to a secure location immediately after issuance (cannot be viewed again)
Security Best Practices
Regular Key Replacement Recommended Frequency:
Production: Every 3 months
Staging: Every 6 months
Development: Yearly or as needed
Rotation Procedure
Create New Key
Create a new key while keeping the existing one active.
Deploy New Key
# Add new key to environment variables
AWS_ACCESS_KEY_ID = your-new-aws-key-id
AWS_SECRET_ACCESS_KEY = your-new-aws-secret-key
# Restart application
Monitor
Verify it works without errors for 24 hours
Delete Old Key
Deactivate/delete the old key if no issues
To perform key rotation without downtime, use a blue-green approach that maintains both new and old keys simultaneously.
Principle of Least Privilege
Minimize IAM Policies // ❌ Bad example: All permissions
{
"Effect" : "Allow" ,
"Action" : "s3:*" ,
"Resource" : "*"
}
// ✅ Good example: Only necessary permissions
{
"Effect" : "Allow" ,
"Action" : [
"s3:PutObject" ,
"s3:GetObject"
],
"Resource" : "arn:aws:s3:::my-bucket/uploads/*"
}
Separate Read-Only Keys # Write permissions (application)
AWS_WRITE_ACCESS_KEY_ID = your-write-key-id
AWS_WRITE_SECRET_ACCESS_KEY = your-write-secret-key
# Read permissions (analytics/backup)
AWS_READ_ACCESS_KEY_ID = your-read-key-id
AWS_READ_SECRET_ACCESS_KEY = your-read-secret-key
Invalidate Key Immediately
# AWS CLI
aws iam delete-access-key \
--access-key-id your-leaked-access-key-id \
--user-name sonamu-user
# OpenAI Platform
# API keys → Click Revoke
Issue and Deploy New Key
Issue a new key immediately and deploy to production
Check Usage History
# Check for suspicious activity in AWS CloudTrail
# Check for abnormal calls on OpenAI Usage page
Clean Git History
# Remove key from commit history (carefully!)
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch .env" \
--prune-empty --tag-name-filter cat -- --all
# Force push
git push origin --force --all
Cleaning Git history affects collaborating team members. Coordinate with your team before proceeding.
# Install git-secrets
brew install git-secrets
# Apply to project
git secrets --install
git secrets --register-aws
# Scan
git secrets --scan
Environment Variable Encryption
Development Environment # Using dotenv-vault
npm install -g dotenv-vault
# Encrypt
npx dotenv-vault encrypt
# Decrypt (team members)
DOTENV_KEY = your-dotenv-vault-key npx dotenv-vault decrypt
Production Environment AWS Systems Manager Parameter Store: import { SSMClient , GetParameterCommand } from "@aws-sdk/client-ssm" ;
const client = new SSMClient ({ region: "ap-northeast-2" });
async function getSecret ( name : string ) {
const command = new GetParameterCommand ({
Name: name ,
WithDecryption: true ,
});
const response = await client . send ( command );
return response . Parameter ?. Value ;
}
// Usage
const apiKey = await getSecret ( "/myproject/prod/openai-api-key" );
HashiCorp Vault: import vault from "node-vault" ;
const client = vault ({
endpoint: "https://vault.example.com:8200" ,
token: process . env . VAULT_TOKEN ,
});
const { data } = await client . read ( "secret/data/myproject/prod" );
const apiKey = data . data . OPENAI_API_KEY ;
Key Usage Monitoring
AWS CloudWatch
// Monitor usage with AWS SDK
import { CloudWatchClient , GetMetricStatisticsCommand } from "@aws-sdk/client-cloudwatch" ;
const client = new CloudWatchClient ({ region: "ap-northeast-2" });
const stats = await client . send (
new GetMetricStatisticsCommand ({
Namespace: "AWS/S3" ,
MetricName: "NumberOfObjects" ,
Dimensions: [{
Name: "BucketName" ,
Value: "my-project-uploads" ,
}],
StartTime: new Date ( Date . now () - 86400000 ), // 24 hours ago
EndTime: new Date (),
Period: 3600 , // 1 hour
Statistics: [ "Average" ],
})
);
OpenAI Usage Alerts
// Check usage and send alerts
import OpenAI from "openai" ;
const openai = new OpenAI ({ apiKey: process . env . OPENAI_API_KEY });
// Run periodically (cron job)
async function checkUsage () {
const usage = await fetch ( "https://api.openai.com/v1/usage" , {
headers: {
Authorization: `Bearer ${ process . env . OPENAI_API_KEY } ` ,
},
}). then ( r => r . json ());
const totalCost = usage . total_usage * 0.0001 ; // Example rate
if ( totalCost > 80 ) {
// Send warning notification (Slack, Email, etc.)
console . warn ( `⚠️ OpenAI usage warning: $ ${ totalCost } ` );
}
}
Test Environment Key Management
# .env.test
OPENAI_API_KEY = test-mock-key
AWS_ACCESS_KEY_ID = test-key-id
AWS_SECRET_ACCESS_KEY = test-secret-key
Use mocks in tests to avoid calling actual APIs. This reduces test costs and improves speed.
Troubleshooting
Symptoms: Error: The AWS Access Key Id you provided does not exist in our records
Causes:
Incorrect Access Key ID
Key has been deleted or deactivated
Typos (spaces, line breaks)
Solution: # Verify key
echo $AWS_ACCESS_KEY_ID
# Check .env file
cat .env | grep AWS
# Check key status in IAM console
# Verify it's in Active status
Symptoms: RateLimitError: Rate limit reached for gpt-4
Causes:
API request limit exceeded (RPM, TPM)
Solution: // Implement retry logic
import { RateLimiter } from "limiter" ;
const limiter = new RateLimiter ({
tokensPerInterval: 10 ,
interval: "minute" ,
});
async function callOpenAI ( prompt : string ) {
await limiter . removeTokens ( 1 );
try {
return await openai . chat . completions . create ({
model: "gpt-4" ,
messages: [{ role: "user" , content: prompt }],
});
} catch ( error ) {
if ( error . status === 429 ) {
// Retry after 1 minute
await new Promise ( resolve => setTimeout ( resolve , 60000 ));
return callOpenAI ( prompt );
}
throw error ;
}
}
Symptoms: Access Denied: You do not have permission to perform this action
Causes:
IAM policy missing required permissions
Blocked by bucket policy
Solution: // Add permissions to IAM policy
{
"Effect" : "Allow" ,
"Action" : [
"s3:PutObject" ,
"s3:GetObject" ,
"s3:DeleteObject"
],
"Resource" : "arn:aws:s3:::my-bucket/*"
}
# Test permissions
aws s3 ls s3://my-bucket --profile myproject
Next Steps