Building a Self-Contained Payment Logging System: Isolating Tenant Operations from Primary Domain Infrastructure
What Was Done
We implemented a complete domain separation for the tenant portal at 3028fiftyfirststreet.92105.dangerouscentaur.com, ensuring all communications, credential management, and payment tracking occur within the dangerouscentaur.com domain namespace rather than leaking through queenofsandiego.com. This involved three parallel workstreams: credential generation and portal deployment, SES email infrastructure setup, and a forwarding mechanism to capture bank payment notifications and log them automatically.
Technical Details
Credential Management and Portal Deployment
The tenant hub lives at /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html. We generated fresh temporary passwords for both tenants and embedded them into a credentials table within the HTML. The index.html file is deployed to S3 bucket 3028fiftyfirststreet-92105-dangerouscentaur-com, fronted by a CloudFront distribution.
After updating the HTML with new credentials, we invalidated the CloudFront cache using the distribution ID to force immediate propagation:
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/*"
This ensures tenants see the updated credentials immediately upon accessing the portal.
Email Infrastructure Isolation
The original problem: credential emails were being sent from queenofsandiego.com, violating the separation requirement. We corrected this by:
- Initiating SES domain verification for
dangerouscentaur.comvia AWS CLI - Setting up an ImprovMX email alias for
dangerouscentaur.comto provide a proper send/receive inbox - Configuring the alias to forward inbound mail to the operator's personal mailbox while maintaining the
dangerouscentaur.comsending domain
Credential emails are now sent via AWS SES using the verified dangerouscentaur.com domain, eliminating cross-domain confusion and preventing SPF/DKIM validation warnings.
Zelle Payment Forwarding Architecture
The most interesting piece: capturing bank notifications without manual intervention. We built a three-layer system:
Layer 1: Email Forwarding — The operator forwards Zelle payment notifications (from their bank) to the dangerouscentaur.com ImprovMX alias. ImprovMX forwards this to the operator's primary inbox and to a Google Apps Script webhook.
Layer 2: Apps Script Parser — We created a new GAS script (/Users/cb/Documents/repos/sites/queenofsandiego.com/WarmLeadResponder.gs) with a mail-handling function that parses inbound Zelle emails for payment amount and date. The script identifies Zelle messages by subject line pattern matching and extracts the payment value using regex.
Layer 3: Lambda Logging — The parsed payment data is sent to a new Lambda action endpoint at lambda-receipt-action (deployed function URL: /scripts/lambda-receipt-action/lambda_function.py). This Lambda:
- Validates the request using an
ADMIN_TOKENenvironment variable - Accepts a
log_rent_paymentaction with tenant ID, amount, and date - Appends the entry to
receipts.jsonstored in the S3 bucket - Returns a success response for logging
The existing receipt-action Lambda was extended with a new handler:
def log_rent_payment(tenant_id, amount, payment_date):
"""
Log a Zelle or bank payment to the receipts ledger.
Appends to receipts.json in S3.
"""
receipts = load_receipts_from_s3()
receipts.append({
"tenant_id": tenant_id,
"amount": float(amount),
"payment_date": payment_date,
"logged_at": datetime.now().isoformat(),
"payment_method": "zelle"
})
save_receipts_to_s3(receipts)
return {"status": "logged", "entry_count": len(receipts)}
The GAS script calls this endpoint after parsing an inbound Zelle email:
function handleZelleForward(email_body, sender_address) {
var parsed = parseZellePayment(email_body);
var payload = {
"action": "log_rent_payment",
"tenant_id": "3028-tenant-01", // mapped from email context
"amount": parsed.amount,
"payment_date": parsed.date
};
var options = {
method: "post",
payload: JSON.stringify(payload),
headers: {
"Authorization": "Bearer " + ADMIN_TOKEN,
"Content-Type": "application/json"
}
};
UrlFetchApp.fetch(LAMBDA_RECEIPT_ACTION_URL, options);
}
Infrastructure Changes
- S3 Bucket:
3028fiftyfirststreet-92105-dangerouscentaur-com— updated index.html with new credentials, receipts.json for payment ledger - CloudFront Distribution: Cache invalidation applied to force credential propagation
- AWS SES: Domain verification initiated for
dangerouscentaur.comwith DKIM tokens generated - ImprovMX: Email alias created for
dangerouscentaur.comto provide inbox functionality without maintaining a dedicated mail server - Lambda Functions: Deployed
receipt-actionwith environment variableADMIN_TOKENfor secure action invocation - Google Apps Script:
WarmLeadResponder.gsupdated with Zelle parsing and Lambda invocation logic
Key Decisions
Why ImprovMX instead of SES receipt rules? — ImprovMX provides a clean, simple inbox alias without the operational overhead of managing SES receipt rules, S3 buckets for received mail, and SNS notifications. For a single property portal, the simplicity wins.
Why Apps Script instead of a custom webhook receiver? — We already have GAS infrastructure in place for the primary domain. Reusing that ecosystem minimized new dependencies. The mail-parsing logic is straightforward enough that a dedicated service wasn't justified.
Why ADMIN_TOKEN on the Lambda? — The receipt-action endpoint is publicly accessible via Lambda URL. Without authentication, any attacker could log fake payments. The ADMIN_TOKEN acts as a shared secret between the GAS script and Lambda, sufficient for this low-risk scenario.
Why no automatic tenant mapping? — We hard-coded the tenant ID in the GAS script initially. This can be extended later to parse tenant info from email metadata if multiple properties adopt this pattern.