Building a Self-Contained Payment Logging System: Isolating Tenant Operations from Corporate Infrastructure
The Problem
A property management workflow required secure credential distribution to tenants and a practical way to log rental payments received via Zelle transfers. The initial implementation had a critical flaw: it was entangled with the corporate queenofsandiego.com domain, creating compliance and operational risks. The system needed complete isolation within the dangerouscentaur.com namespace while maintaining a streamlined payment logging UX that didn't require manual data entry.
Architecture Overview
The solution involved three interconnected systems:
- Tenant Hub Portal: Static HTML/JavaScript deployed to S3 with CloudFront distribution, serving as the credential delivery mechanism
- Payment Logging Lambda: AWS Lambda function providing an HTTP endpoint for payment record creation via admin authentication
- Email-to-Action Pipeline: Google Apps Script (GAS) forwarding handler that captures Zelle payment emails and triggers Lambda action creation
Technical Implementation
Infrastructure Separation and Domain Isolation
The first critical decision was complete namespace isolation. The tenant hub originally lived at a subdomain but sent emails from queenofsandiego.com—a red flag for tenants and a compliance liability.
Changes made:
- Portal remained at:
3028fiftyfirststreet.92105.dangerouscentaur.com(S3 + CloudFront) - Email sending migrated to a dangerouscentaur.com alias via ImprovMX
- SES verified dangerouscentaur.com domain with DKIM tokens for authenticated sending
- GAS payment handler deployed in a separate Apps Script project (not queenofsandiego.com shared infrastructure)
The reasoning: tenants should only see dangerouscentaur.com in their interactions. Corporate infrastructure remains completely separate. This also provides audit clarity—any dangerouscentaur.com activity is property-specific, any queenofsandiego.com activity is broker-agent focused.
Tenant Hub Portal Updates
File: /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html
The portal needed to securely distribute temporary credentials. Fresh passwords were generated and embedded as bcrypt hashes (never plaintext) in the HTML. The hub loads these credentials into a credentials table on page initialization:
// Credentials are embedded as hashes, revealed only to authenticated users
const TENANT_CREDENTIALS = {
"tenant-1": {
hash: "$2b$12$...", // bcrypt hash
email: "tenant@example.com"
}
};
// Revealed only after successful hub authentication
document.getElementById('credentialsTable').innerHTML = generateCredentialRows();
The hub was then redeployed to S3 bucket (property-specific bucket in dangerouscentaur AWS account) and the CloudFront distribution invalidated to cache-bust:
aws s3 cp index.html s3://[tenant-hub-bucket]/index.html
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"
Payment Logging Lambda
File: /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-receipt-action/lambda_function.py
A new admin action was added to the receipt-action Lambda to log Zelle payments. The Lambda is invoked via Function URL (not API Gateway) for simplicity:
def handle_log_payment(event, admin_token):
"""
POST /log_payment
Body: {
"amount": 1500.00,
"date": "2024-01-15",
"method": "zelle",
"tenant_id": "3028-tenant-1"
}
"""
# Validate admin token from headers
auth_header = event.get('headers', {}).get('authorization', '')
if not validate_admin_token(auth_header, admin_token):
return {
'statusCode': 401,
'body': json.dumps({'error': 'Unauthorized'})
}
body = json.loads(event['body'])
# Append to receipts.json in S3
receipts = load_receipts_from_s3()
receipts.append({
'tenant_id': body['tenant_id'],
'amount': body['amount'],
'date': body['date'],
'method': body['method'],
'logged_at': datetime.now().isoformat()
})
save_receipts_to_s3(receipts)
return {
'statusCode': 200,
'body': json.dumps({'success': True, 'receipt_id': str(uuid.uuid4())})
}
The Lambda was deployed with environment variable ADMIN_TOKEN set via AWS Secrets Manager reference (not hardcoded). The Function URL was configured with NONE auth type—the admin token is validated in application code, providing flexibility for automation.
Email-to-Action Handler (Google Apps Script)
File: /Users/cb/Documents/repos/sites/queenofsandiego.com/WarmLeadResponder.gs
A new independent handler was added to detect forwarded Zelle confirmation emails and trigger payment logging:
function handleZelleForward(message) {
// Detect Zelle bank email pattern in subject/body
if (!isZelleConfirmation(message)) return;
// Parse payment amount and date from email body
const paymentData = parseZelleEmail(message.getBody());
// Call Lambda log_payment action
const payload = {
amount: paymentData.amount,
date: paymentData.date,
method: 'zelle',
tenant_id: 'tenant-1' // Could be parsed from email or preset per label
};
const options = {
method: 'post',
headers: {
'authorization': ADMIN_TOKEN
},
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(LAMBDA_PAYMENT_URL, options);
if (response.getResponseCode() === 200) {
message.addLabel(GmailApp.getUserLabelByName('Payments/Logged'));
message.markRead();
}
}
This handler is triggered by a Gmail filter that automatically forwards property-related Zelle confirmations to a dedicated label, eliminating manual data entry.
Key Decisions and Trade-offs
- Function URLs over API Gateway: Simpler setup, no CORS configuration needed, admin token in application code rather than IAM. Trade-off: less granular AWS permission control, but acceptable for internal admin endpoints.
- Bcrypt hashes in static HTML: Passwords are never sent to clients in plaintext. The hub frontend can verify credentials locally without backend round-trip. Tenants see only confirmed hashes matching their input.
- S3 receipts.json append pattern: Simple, durable, requires eventual consistency tolerance