Building a Self-Contained Payment Logging System: Divorcing Tenant Infrastructure from Marketing Domain
What Was Done
We built a complete payment verification and logging system for a rental property tenant portal, with a critical requirement: complete separation from the marketing domain (queenofsandiego.com). This involved:
- Generating fresh tenant credentials and redeploying the tenant hub portal at
3028fiftyfirststreet.92105.dangerouscentaur.com - Setting up SES email infrastructure on the
dangerouscentaur.comdomain (completely separate from the marketing domain) - Building a Lambda-based payment logging pipeline that accepts forwarded bank emails and automatically logs Zelle payments
- Creating a Google Apps Script webhook receiver to parse payment notifications and trigger Lambda functions
- Establishing an admin token authentication scheme to secure the payment logging endpoint
Technical Architecture
Domain Separation Pattern
The core architectural decision was strict domain isolation. All tenant-facing infrastructure runs under dangerouscentaur.com, while queenofsandiego.com remains purely for marketing. This separation provides:
- Security boundary: Tenant data and accounting operations are completely isolated from public-facing marketing infrastructure
- Email reputation: Tenant communications use dedicated SES-verified domain credentials, preventing marketing email issues from affecting critical tenant communications
- Operational clarity: Each domain has its own CloudFront distributions, S3 buckets, and Lambda functions with no cross-domain dependencies
Tenant Hub Portal Structure
The tenant portal lives in:
/Users/cb/Documents/repos/sites/dangerouscentaur/demos/
3028fiftyfirststreet.92105.dangerouscentaur.com/
This directory contains:
index.html— Main portal interface with embedded tenant credentials (hashed passwords)scripts/lambda-receipt-action/lambda_function.py— AWS Lambda that logs payment eventsscripts/lambda-email-parser/lambda_function.py— AWS Lambda that parses forwarded bank emails
The portal is served via S3 with CloudFront CDN. We identified the CloudFront distribution using the AWS CLI and invalidated the full cache after each deployment to ensure fresh credentials reached tenants immediately.
Password Generation and Deployment
Fresh temporary passwords were generated and embedded in index.html with bcrypt hashing (not plaintext). The updated portal was deployed using:
aws s3 cp index.html s3://[bucket-name]/index.html
aws cloudfront create-invalidation --distribution-id [DIST-ID] --paths "/*"
SES Email Infrastructure
AWS SES was configured to send emails from the dangerouscentaur.com domain. We:
- Verified the domain with SES (DKIM tokens added to DNS)
- Created ImprovMX aliases to receive payment notifications at a dedicated inbox
- Sent credential emails via SES SDK with proper sender configuration (no more queenofsandiego.com domain in tenant emails)
Email was sent using AWS SES directly from Python/Node.js code, ensuring proper domain authentication headers and deliverability.
Payment Logging Pipeline
Lambda: Receipt Action Handler
File: scripts/lambda-receipt-action/lambda_function.py
This Lambda function is the core of the payment system. It:
- Exposes an HTTP endpoint via AWS Lambda Function URL
- Validates incoming requests using an
ADMIN_TOKENenvironment variable - Accepts JSON payloads with payment details (tenant ID, amount, payment method, date)
- Appends payment records to
receipts.jsonstored in S3 - Returns structured JSON responses for logging and auditing
The function URL was configured with QUALIFIER=LIVE and resource-based policy to accept only authenticated requests:
def handler(event, context):
# Extract admin token from request headers
token = event.get('headers', {}).get('Authorization', '')
if token != os.environ.get('ADMIN_TOKEN'):
return {
'statusCode': 401,
'body': json.dumps({'error': 'Unauthorized'})
}
body = json.loads(event.get('body', '{}'))
# Log payment to receipts.json in S3
payment_record = {
'tenant_id': body.get('tenant_id'),
'amount': body.get('amount'),
'method': body.get('method'), # 'zelle', 'check', etc.
'date': datetime.now().isoformat()
}
# Read current receipts, append, write back
# (Includes proper S3 retry logic)
return {
'statusCode': 200,
'body': json.dumps({'success': True, 'record_id': ...})
}
Lambda: Email Parser
File: scripts/lambda-email-parser/lambda_function.py
This Lambda parses forwarded Zelle confirmation emails and extracts payment details (amount, date, reference number). It calls the receipt-action Lambda with proper authentication headers.
Google Apps Script: Webhook Receiver
File: WarmLeadResponder.gs (modified)
The GAS script was enhanced to:
- Receive forwarded emails from the owner's bank inbox
- Detect payment-related subject lines and body content (Zelle confirmations)
- Extract amount, date, and sender details using regex patterns
- Call the Lambda receipt-action endpoint with the
ADMIN_TOKENheader - Log results to a Google Sheet for owner visibility
The GAS command handler was wired to intercept emails matching payment patterns:
function doPost(e) {
var data = JSON.parse(e.postData.contents);
if (isProbablyPayment(data.subject, data.body)) {
var paymentInfo = parseZelleEmail(data.body);
var response = callLambda(
'log_rent_payment',
{
tenant_id: extractTenantId(data),
amount: paymentInfo.amount,
method: 'zelle',
date: paymentInfo.date
}
);
logToSheet('Payments', response);
}
}
Infrastructure Components
- S3 Bucket: Stores tenant portal HTML, assets, and
receipts.json - CloudFront Distribution: Serves the tenant portal with edge caching
- Lambda Functions: Two separate functions with unique URLs for receipt logging and email parsing
- SES