A serverless email API service for handling form submissions from client websites. Currently supports AWS Lambda + SES, with plans for multi-provider support.
OSS Forms API provides a simple, cost-effective way to handle contact forms and other form submissions without managing servers. Perfect for static sites, portfolios, and any application needing reliable form processing.
- π Serverless: Deploy on AWS Lambda for automatic scaling
- π§ Email Delivery: Reliable sending via AWS SES
- π API Key Authentication: Secure access control for multiple clients
- π‘οΈ Rate Limiting: Configurable limits to prevent abuse
- β Input Validation: Comprehensive validation and sanitization
- π CORS Support: Ready for browser-based form submissions
- π° Cost Effective: Pay only for actual usage
- π§ Easy Integration: Works with any frontend framework
- π§ Multi-Provider Email: SendGrid, Mailgun, Postmark, Resend, SMTP support
- βοΈ Multi-Cloud Deploy: Vercel, Netlify Functions, Google Cloud Functions
- π§ Enhanced Auth: JWT tokens, webhook signatures
- π Analytics: Form submission metrics and insights
- π‘οΈ Advanced Security: Spam protection, enhanced validation
- π§ͺ Testing Suite: Comprehensive unit and integration tests
- Python 3.9+
- Node.js 14+ (for Serverless Framework)
- AWS CLI configured with appropriate permissions
- AWS account with SES access
- Free Serverless Framework account (required for deployment)
# Fork this repository on GitHub first, then:
git clone https://github.com/YOUR-USERNAME/oss-forms-api.git
cd oss-forms-api
# (Recommended) Create a Python virtual environment
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
# Install Python dependencies
pip install -r requirements.txt
# Install Node.js dependencies
npm install
# Install Serverless Framework globally (if not already installed)
npm install -g serverlessBefore deploying, configure AWS SES in your target region:
# 1. Set your AWS region (must match serverless.yml region)
export AWS_REGION=us-east-2
# 2. Verify your sender email address or domain
aws ses verify-email-identity --email-address noreply@yourdomain.com --region $AWS_REGION
# 3. Check verification status (wait for "Success")
aws ses get-identity-verification-attributes --identities noreply@yourdomain.com --region $AWS_REGION
# 4. If in SES sandbox, verify recipient emails for testing
aws ses verify-email-identity --email-address test@yourdomain.com --region $AWS_REGIONImportant: The SES verification must be done in the same AWS region where you deploy the Lambda function. If you change regions later, you'll need to re-verify your email in the new region.
IMPORTANT: Set these environment variables before deployment. The deployment will fail without them.
# Required - API keys for client authentication
# Each key must be at least 16 characters long
export VALID_API_KEYS=your-secret-key-12345,another-secret-key-67890
# Recommended - Your verified SES sender email
export SES_DEFAULT_SENDER=noreply@yourdomain.com
# Optional - AWS region (defaults to us-east-2 in serverless.yml)
export AWS_REGION=us-east-2API Key Requirements:
- Minimum 16 characters per key
- Use strong, random strings
- Separate multiple keys with commas (no spaces)
- These will be used by your frontend to authenticate API calls
Example of generating secure API keys:
# Generate random 32-character keys
openssl rand -hex 16 # outputs: a1b2c3d4e5f6789012345678901234567890abcd# Test locally (optional)
python app/main.py
# API available at http://localhost:5000The project uses Serverless Framework for AWS deployment, which simplifies Lambda and API Gateway configuration. As of v4, Serverless Framework requires a free account:
# First-time setup: Login to Serverless Framework (free)
serverless login
# The login is required because Serverless Framework:
# - Handles complex AWS Lambda + API Gateway configuration
# - Manages deployment packaging and dependencies
# - Provides infrastructure-as-code via serverless.yml
# - Simplifies CORS, environment variables, and IAM permissionsWhy Serverless Framework?
- Simplified deployment: Converts
serverless.ymlinto CloudFormation templates - Automatic CORS setup: Handles browser compatibility for form submissions
- Dependency management: Packages Python dependencies correctly for Lambda
- Environment isolation: Easy dev/staging/prod deployments
Option 1: Using the deployment script (recommended)
# Make sure environment variables are set first
export VALID_API_KEYS=your-secret-key-12345,another-secret-key-67890
export SES_DEFAULT_SENDER=noreply@yourdomain.com
# Deploy to development
./deploy.sh dev
# Deploy to production
./deploy.sh prodOption 2: Direct serverless commands
# Make sure environment variables are set first
export VALID_API_KEYS=your-secret-key-12345,another-secret-key-67890
export SES_DEFAULT_SENDER=noreply@yourdomain.com
# Deploy to development
serverless deploy --stage dev
# Deploy to production
serverless deploy --stage prodAfter deployment:
- Note the API endpoint URL from the deployment output
- Test the health endpoint:
curl -X GET https://your-api-url/health - Save your API keys securely for frontend integration
If you want to contribute to the project:
# Fork the repo, then clone your fork
git clone https://github.com/YOUR-USERNAME/oss-forms-api.git
cd oss-forms-api
# Install development dependencies
pip install -r requirements.txt
pip install pytest black flake8 # Additional dev tools
# Install Node.js dependencies
npm install
# Create a feature branch
git checkout -b feature/your-feature-name
# Make your changes and test
python -m pytest # Run tests (if available)
# Follow contribution guidelines in CONTRIBUTING.mdThis project is designed to be easily forkable for your own needs:
- Customize validation rules for your specific forms
- Add custom email templates for your brand
- Integrate with your specific services (CRM, Slack, etc.)
- Modify rate limiting for your traffic patterns
- Add custom authentication (OAuth, custom headers, etc.)
- Email templates: Modify
_generate_email_body()inapp/handlers/email_handler.py:120 - Validation rules: Update
app/utils/validation.py - Rate limits: Adjust
app/utils/rate_limiter.py - Authentication: Extend
app/auth/api_key_auth.py - Response format: Customize responses in
app/main.py
- Keep upstream connection: Add this repo as an upstream remote
- Document your changes: Update README with your modifications
- Environment-specific config: Use environment variables for customization
- Consider contributing back: If your changes could benefit others, submit a PR!
All requests require an API key in the X-API-Key header:
curl -X POST https://your-api-url/submit-form \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d @form-data.jsonPOST /submit-form
{
"to_email": "recipient@example.com",
"from_email": "sender@example.com",
"subject": "New Contact Form Submission",
"source_url": "https://yoursite.com/contact",
"fields": {
"name": "John Doe",
"email": "john@example.com",
"message": "Hello, I'd like to get in touch!",
"phone": "+1-555-123-4567"
}
}to_email: Recipient email addressfields: Object containing form data (at least one field required)
from_email: Sender email (defaults to noreply@example.com)subject: Email subject (defaults to "New Form Submission")source_url: URL where the form was submitted from
Success (200)
{
"message": "Form submitted successfully",
"email_id": "resend-email-id"
}Error (400/401/429/500)
{
"error": "Error description"
}GET /health
{
"status": "healthy",
"service": "mayfly-forms"
}Default rate limits per IP address:
- 10 requests per minute
- 100 requests per hour
- 1,000 requests per day
Rate limit exceeded returns HTTP 429.
<form id="contact-form">
<input type="text" name="name" required>
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>
<script>
document.getElementById('contact-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
to_email: 'contact@yourcompany.com',
subject: 'New Contact Form Submission',
source_url: window.location.href,
fields: Object.fromEntries(formData)
};
try {
const response = await fetch('https://your-api-url/submit-form', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key'
},
body: JSON.stringify(data)
});
if (response.ok) {
alert('Message sent successfully!');
e.target.reset();
} else {
alert('Error sending message. Please try again.');
}
} catch (error) {
alert('Network error. Please try again.');
}
});
</script>import { useState } from 'react';
const ContactForm = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch('https://your-api-url/submit-form', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.REACT_APP_FORMS_API_KEY
},
body: JSON.stringify({
to_email: 'contact@yourcompany.com',
subject: 'New Contact Form Submission',
source_url: window.location.href,
fields: formData
})
});
if (response.ok) {
alert('Message sent successfully!');
setFormData({ name: '', email: '', message: '' });
} else {
alert('Error sending message.');
}
} catch (error) {
alert('Network error.');
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
{/* Form inputs */}
</form>
);
};| Variable | Required | Description | Example |
|---|---|---|---|
VALID_API_KEYS |
Yes | Comma-separated list of valid API keys for clients | client-key-1,client-key-2 |
SES_DEFAULT_SENDER |
Recommended | Default sender email (must be verified in SES) | noreply@yourdomain.com |
AWS_REGION |
No | AWS region for SES (defaults to us-east-1) | us-west-2 |
FLASK_ENV |
No | Flask environment (for local development only) | development |
Note: The table in the original README mentioned RESEND_API_KEY but the code actually uses AWS SES. This will change when multi-provider support is added.
Edit app/utils/rate_limiter.py to customize rate limits:
self.limits = {
'per_minute': 10, # Requests per minute
'per_hour': 100, # Requests per hour
'per_day': 1000 # Requests per day
}- API Key Authentication: Secure access control with configurable keys
- Input Validation: Comprehensive validation and sanitization of all form data
- XSS Protection: Automatic filtering of potentially malicious content
- Rate Limiting: Built-in protection against abuse (10 req/min, 100 req/hour, 1000 req/day per IP)
- CORS Configuration: Configurable cross-origin resource sharing
- Secure Defaults: Debug mode disabled by default, no hardcoded secrets
- Generate Strong Keys: Use keys with 16+ characters, include random alphanumeric characters
- Keep Keys Secret: Never commit API keys to version control or share publicly
- Use Environment Variables: Always configure
VALID_API_KEYSvia environment variables - Consider Rotation: Implement key rotation for production deployments
- Restrict Origins: For production, update CORS settings to only allow your specific domains
- Review Regularly: Audit which domains have access to your API
- Monitor Usage: Watch CloudWatch logs for unusual patterns or abuse
- Set Up Alerts: Configure AWS CloudWatch alarms for high request volumes
- Review Access: Regularly audit API key usage patterns
- Distributed Limits: Current rate limiting is per-Lambda instance; consider DynamoDB for distributed limiting in high-traffic scenarios
- IP Spoofing: Be aware that
X-Forwarded-Forheaders can be manipulated - Adjust Limits: Modify rate limits in
app/utils/rate_limiter.pybased on your needs
- IAM Permissions: Use minimal required permissions for Lambda execution role
- SES Sandbox: Remove SES sandbox limitations only after verifying your sending domain
- VPC Configuration: Consider deploying Lambda in VPC for additional network isolation
- Encryption: Enable encryption at rest for any stored data
Error: Serverless Framework V4 CLI requires an account or license key
Solution:
serverless loginThis creates a free account. The dashboard is optional for monitoring but login is required for deployment.
Error: VALID_API_KEYS environment variable is required
Solution:
export VALID_API_KEYS=your-secret-key-12345,another-secret-key-67890
export SES_DEFAULT_SENDER=noreply@yourdomain.comError: API key too short: another-... (minimum 16 characters required)
Solution: Each API key must be at least 16 characters:
# Bad (too short)
export VALID_API_KEYS=key1,key2
# Good (16+ characters each)
export VALID_API_KEYS=your-secret-key-12345,another-secret-key-67890Error: {"message": "Internal server error"} when testing endpoints
Common causes:
-
SES email not verified in deployment region
# Verify in correct region aws ses verify-email-identity --email-address your-email@domain.com --region us-east-2 -
Wrong AWS region configuration
- Check
serverless.ymlregion setting - Verify SES email in same region
- Update region if needed:
serverless deploy --stage dev --region us-east-2
- Check
Error: Cannot resolve '${env:VALID_API_KEYS}' variable
Solution: Environment variables aren't available to the serverless command:
# Set variables before each command
VALID_API_KEYS=your-keys SES_DEFAULT_SENDER=your-email serverless deployIssue: Deployed to wrong region (e.g., us-east-1 instead of us-east-2)
Solution:
- Update
serverless.ymlregion setting - Re-verify SES email in new region
- Deploy again to new region
- Optionally remove old deployment:
serverless remove --stage dev --region us-east-1
Issue: Emails only work with verified recipient addresses
Solution:
- For testing: Verify recipient emails in SES
- For production: Request SES production access via AWS Support
Error: AWS permission errors during deployment
Solution: Ensure your AWS CLI user has these permissions:
- Lambda full access
- API Gateway full access
- SES full access (or send permissions)
- CloudFormation full access
- IAM role creation permissions
AWS Lambda:
- Free tier: 1M requests/month
- After free tier: $0.20 per 1M requests
AWS SES:
- Free tier: 62,000 emails/month (when sent from EC2)
- Outside free tier: $0.10 per 1,000 emails
- Additional charges for attachments: $0.12 per GB
Example monthly costs:
- Under 62K form submissions: Free (within SES free tier)
- 100K form submissions: ~$4 (Lambda + SES)
- 500K form submissions: ~$45 (Lambda + SES)
Significantly more cost-effective than third-party email services!
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Client Apps βββββΆβ Mayfly Forms βββββΆβ AWS SES β
β (Static sites, β β (Lambda) β β (Email Send) β
β React, etc.) β β β β β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
oss-forms-api/
βββ app/ # Core Flask application
β βββ handlers/
β β βββ email_handler.py # π― AWS SES integration (make generic!)
β βββ auth/
β β βββ api_key_auth.py # API key authentication
β βββ utils/
β β βββ validation.py # Input validation
β β βββ rate_limiter.py # Rate limiting logic
β βββ main.py # Flask application entry point
βββ docs/ # Documentation
β βββ DEPLOYMENT_GUIDE.md # Deployment instructions
β βββ DELIVERABILITY_GUIDE.md # Email deliverability setup
β βββ SES_SETUP.md # AWS SES specific setup
βββ lambda_function.py # AWS Lambda entry point
βββ serverless.yml # Serverless deployment config
βββ requirements.txt # Python dependencies
βββ CONTRIBUTING.md # Contribution guidelines
βββ example-client.html # Frontend integration example
βββ README.md # This file
# Install test dependencies (when available)
pip install pytest
# Run tests
pytest
# Run with coverage
pytest --cov=app/# Format code
black app/
# Lint code
flake8 app/
# Type checking (if types added)
mypy app/# Generate new API keys
from app.auth.api_key_auth import generate_api_key
new_key = generate_api_key('client-name')
print(new_key) # Returns: client-name-<random-string>We need your help! Currently, Mayfly Forms only supports AWS SES. We want to make it work with multiple email providers.
- SendGrid - Popular email service with good API
- Mailgun - Developer-friendly email service
- Postmark - High deliverability focus
- Resend - Modern email API
- SMTP - Generic SMTP support for any provider
We need to create a generic provider interface. The current EmailHandler class in app/handlers/email_handler.py is AWS SES specific.
Proposed structure:
# app/providers/base.py
class EmailProvider:
def send_email(self, form_data) -> dict:
"""Send email via provider. Returns success/error status."""
raise NotImplementedError
# app/providers/ses_provider.py
class SESProvider(EmailProvider):
# Move current SES logic here
# app/providers/sendgrid_provider.py
class SendGridProvider(EmailProvider):
# Implement SendGrid integrationAdd provider selection via environment variables:
EMAIL_PROVIDER=ses # or sendgrid, mailgun, etc.
SENDGRID_API_KEY=... # Provider-specific config
MAILGUN_API_KEY=...Create a factory to instantiate the correct provider:
# app/providers/factory.py
def get_email_provider():
provider = os.environ.get('EMAIL_PROVIDER', 'ses')
if provider == 'ses':
return SESProvider()
elif provider == 'sendgrid':
return SendGridProvider()
# etc.When adding a new provider, please ensure:
- Provider class implements
EmailProviderinterface - Environment variables documented for configuration
- Error handling follows existing patterns
- Tests added for the new provider
- Documentation updated (README + provider-specific guide)
- Example configuration provided
- Rate limiting considerations addressed
- Deliverability features implemented (reply-to, sender name, etc.)
- Check existing issues for provider requests
- Create an issue to discuss the provider you want to add
- Fork the repository and create a feature branch
- Study the current SES implementation in
app/handlers/email_handler.py - Design the provider interface (we can help with this!)
- Implement your provider following the checklist above
- Submit a PR with comprehensive testing
Beyond email providers, we welcome contributions for:
- Deployment options: Vercel, Netlify Functions, Google Cloud Functions
- Authentication methods: JWT tokens, webhook signatures
- Storage backends: For rate limiting, analytics
- Monitoring: Health checks, metrics, logging improvements
- Security: Additional validation, spam protection
- Testing: Unit tests, integration tests, load testing
- π Bug Reports: Create an issue
- π‘ Feature Requests: Create an issue with
enhancementlabel - β Questions: Discussions or create issue with
questionlabel - π€ Contributions: See CONTRIBUTING.md for detailed guidelines
MIT License - see LICENSE file for details.
β Star this repo if it's useful to you! It helps others discover the project.