Building a Domain-Isolated Payment Logging System for Multi-Tenant Property Management
The Challenge: Separating Concerns and Automating Payment Tracking
We needed to solve three interconnected problems for a property management system serving multiple tenants:
- Tenants required secure, domain-appropriate credential distribution for a tenant hub portal
- Manual payment logging via chat was error-prone and lacked auditability
- The system was incorrectly routing communications through the wrong domain, creating maintenance and brand confusion
The core insight: build a payment logging pipeline that mirrors how property managers actually receive payments (bank forwards, text notifications, emails), then automatically inject those into the accounting system without manual intervention.
Technical Architecture: Three Integrated Systems
1. Tenant Credential Distribution Pipeline
First, we regenerated temporary passwords and deployed them securely within the tenant hub. The system modifies /dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html with updated credentials, then syncs to S3:
aws s3 cp index.html s3://dangerouscentaur-sites/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/*"
Then we dispatch credentials via AWS SES from the dangerouscentaur.com domain using a verified ImprovMX alias. This is critical—all tenant-facing communications must originate from the property management domain, not from personal business domains. We verified dangerouscentaur.com in SES and created a mail alias to ensure sender authenticity and deliverability.
2. Domain Isolation and Email Routing
The previous system accidentally sent tenant credentials from queenofsandiego.com, creating several problems:
- Brand confusion—tenants saw mail from an unrelated domain
- SPF/DKIM alignment issues when the sending domain doesn't match the business domain
- Operational coupling—property management code lived in a personal business repository
We established hard separation: all dangerouscentaur.com-related operations (tenant portal, payment processing, accounting) live in the isolated /dangerouscentaur/ repository. Communication flows through SES-verified dangerouscentaur.com aliases only. The queenofsandiego.com Google Apps Script (WarmLeadResponder.gs) now explicitly routes inbound property management emails to the dangerouscentaur payment logging system via API call, rather than handling them directly.
3. Zelle Payment Email Forwarding to Automated Logging
This is the elegant part. When you receive a Zelle payment notification (via bank email or text-to-email), you forward it to a dedicated inbox alias. Here's the flow:
- Incoming: Zelle notification arrives at
payments@dangerouscentaur.com(ImprovMX alias) - Routing: ImprovMX forwards to a Google Apps Script webhook trigger
- Parsing: A custom GAS function in
WarmLeadResponder.gsparses the email, extracting tenant name, amount, and date - Logging: GAS calls an AWS Lambda function at a secured endpoint with an admin token
- Storage: Lambda function
lambda-receipt-actionappends the payment toreceipts.jsonin S3 - Display: The tenant hub's receipts section automatically updates via CloudFront cache invalidation
Implementation Details
Lambda Function: Receipt Action Handler
The core handler lives in /scripts/lambda-receipt-action/lambda_function.py:
def lambda_handler(event, context):
# Validate ADMIN_TOKEN from environment
# Route to appropriate action: log_payment, log_receipt, etc.
# Parse JSON payload
# Append to receipts.json in S3
# Invalidate CloudFront cache
return signed response with audit trail
Environment variables include:
ADMIN_TOKEN— shared secret for GAS-to-Lambda authenticationRECEIPTS_BUCKET— S3 bucket name for payment recordsCLOUDFRONT_DIST_ID— distribution ID for cache invalidation
The function uses resource-based policies (not API keys) to authenticate requests, and every action is logged with timestamp, amount, tenant ID, and payment method.
Google Apps Script: Email Parsing and Action Dispatch
In WarmLeadResponder.gs, we added a handler that triggers on incoming mail to payments@dangerouscentaur.com:
function onEmailReceived(e) {
const email = e.message;
// Check for Zelle payment indicators
if (email.contains("Zelle") && email.contains("payment received")) {
const parsed = parseZelleEmail(email);
const payload = {
action: "log_rent_payment",
tenant_id: parsed.tenant,
amount: parsed.amount,
method: "zelle",
date: parsed.date,
email_id: email.getId()
};
callLambdaWithAdminToken(payload);
}
}
This approach keeps payment logging completely within the dangerouscentaur system—no external APIs, no spreadsheet updates, pure event-driven automation.
Key Infrastructure Decisions
Why S3 + CloudFront for tenant hub? Static hosting with CDN provides fast global delivery, eliminates server management, and lets us invalidate caches atomically after payments are logged. The tenant portal is read-heavy (tenants check their payment status), so this pattern is ideal.
Why Lambda for payment logging instead of direct S3 writes? Lambda gives us a single, versioned endpoint with authentication, audit logging, and the ability to trigger side effects (CloudFront invalidation, email notifications) atomically. Direct S3 writes lack these controls.
Why ImprovMX for email routing? It's a thin, reliable layer that forwards emails to webhooks without needing to manage mail server infrastructure. It decouples email delivery from Lambda availability.
Why separate the domains completely? Operational isolation prevents mistakes (accidentally sending tenant mail from personal domains), ensures compliance with SPF/DKIM, and keeps the codebase organized by responsibility.
Deployment and Testing
The deployment sequence was:
- Update
index.htmlwith new credentials and redeploy to S3 - Send credential emails via SES from verified dangerouscentaur.com alias
- Package and deploy updated
lambda-receipt-actionwithlog_paymenthandler - Set
ADMIN_TOKENenvironment variable on Lambda - Update GAS