Building a Tenant Payment Portal with Email-Forwarding Integration: Separating Concerns Across AWS and Google Apps Script
What Was Done
We built a complete tenant management system that allows property managers to:
- Deploy a secure tenant hub portal at a dedicated domain
- Issue temporary credentials to tenants via verified SES email
- Log rent payments by forwarding Zelle notifications to an email handler
- Automatically parse payment emails and update a centralized receipt ledger
- Maintain strict domain separation (all operations within
dangerouscentaur.com, completely divorced fromqueenofsandiego.com)
The system integrates three core components: a static tenant portal hosted on S3/CloudFront, AWS Lambda functions for payment processing, and Google Apps Script for email command parsing.
Technical Architecture
Tenant Portal Infrastructure
The tenant hub is deployed to s3://dc-sites-tenanthub/ and distributed via CloudFront (distribution ID varies by environment). The portal lives at 3028fiftyfirststreet.92105.dangerouscentaur.com—a property-specific subdomain that allows scaling to multiple properties under the same infrastructure.
The index.html file contains:
- User credential table (name, email, temporary password)
- Dashboard section with receipt history and payment status
- JavaScript that loads receipt data dynamically from the Lambda endpoint
Credentials are generated as follows:
import hashlib
import secrets
# Generate 12-character alphanumeric temporary password
temp_password = secrets.token_urlsafe(9)[:12]
# Create bcrypt-like hash for storage (simplified example)
password_hash = hashlib.pbkdf2_hmac(
'sha256',
temp_password.encode(),
b'portal-salt-value',
100000
).hex()
Payment Logging Lambda
Two Lambda functions handle payment workflows:
lambda-receipt-action (/scripts/lambda-receipt-action/lambda_function.py):
- Exposes a public HTTP endpoint via Lambda Function URL
- Accepts POST requests with payment data (tenant ID, amount, payment method, date)
- Validates requests using an
ADMIN_TOKENenvironment variable - Appends structured records to
receipts.jsonin S3 - Triggers CloudFront invalidation to refresh cached receipt data
lambda-email-parser (/scripts/lambda-email-parser/lambda_function.py):
- Triggered by SES receipt notifications
- Parses forwarded Zelle emails for sender, amount, and date
- Invokes the receipt-action endpoint with extracted payment details
- Handles parsing errors gracefully without breaking the payment pipeline
Both Lambdas are deployed with:
cd /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-receipt-action
zip -r lambda_function.zip lambda_function.py
aws lambda update-function-code \
--function-name log-receipt-action \
--zip-file fileb://lambda_function.zip
SES Domain Setup
Rather than sending credentials from queenofsandiego.com (which caused domain trust issues), we configured SES to send from dangerouscentaur.com:
- Verified
dangerouscentaur.comas a sending domain in SES - Retrieved DKIM tokens via AWS CLI and added them to Namecheap DNS
- Configured an ImprovMX alias to forward tenant inquiry emails to a management inbox
- Set
noreply@dangerouscentaur.comas the credential email sender
Tenant credential emails are sent via:
aws ses send-email \
--from noreply@dangerouscentaur.com \
--to tenant-email@example.com \
--subject "Your Tenant Portal Credentials" \
--text "Portal: https://3028fiftyfirststreet.92105.dangerouscentaur.com\nUsername: [tenant-name]\nPassword: [temp-password]"
Email-to-Payment Pipeline via Google Apps Script
Google Apps Script acts as the bridge between email forwarding and payment logging. When a tenant forwards a Zelle confirmation to payments@dangerouscentaur.com (via ImprovMX), the GAS script:
- Polls Gmail for new messages with subject line patterns matching Zelle notifications
- Extracts sender name, payment amount, and transaction timestamp
- Calls the Lambda receipt-action endpoint with an admin token
- Marks processed emails as read to avoid duplicates
The handler in WarmLeadResponder.gs includes:
function handlePaymentCommand(emailData) {
const adminToken = PropertiesService.getScriptProperties().getProperty('ADMIN_TOKEN');
const payload = {
tenant_id: emailData.tenantId,
amount: emailData.amount,
payment_method: 'zelle',
date: emailData.date
};
const options = {
method: 'post',
payload: JSON.stringify(payload),
headers: { 'Authorization': `Bearer ${adminToken}` },
muteHttpExceptions: true
};
UrlFetchApp.fetch(LAMBDA_RECEIPT_ENDPOINT, options);
}
Key Design Decisions
Domain Separation: By using dangerouscentaur.com exclusively, we avoid email deliverability issues and ensure the system is self-contained. A separate property would get its own subdomain (e.g., 4567oak.92103.dangerouscentaur.com).
Temporary Passwords: Rather than generating permanent credentials, tenants receive time-limited passwords. This encourages them to set their own passwords on first login and reduces the risk of exposed credentials remaining valid indefinitely.
Email Forwarding Over Manual Entry: Asking tenants to forward Zelle notifications is far less friction than asking them to log into a portal and enter payment details. The property manager never needs to manually type payment data.
Centralized Receipt Ledger: All payments write to a single receipts.json file in S3. This serves as the source of truth and allows the portal to render payment history without hitting a database.
Admin Token Authentication: Lambda endpoints are protected by an environment variable token rather than exposing them publicly. The GAS script stores this token securely in Properties, preventing unauthorized payment logging.
Infrastructure Resources
- S3 Bucket:
dc-sites-tenanth