Building a Domain-Isolated Tenant Payment Portal: Multi-Lambda Architecture with Email-to-Payment Forwarding
Overview
We deployed a complete tenant management system for a rental property, architected around a critical security requirement: complete domain isolation from the primary business domain. The system needed to handle tenant credential distribution, secure payment logging, and automated email-based payment intake—all within the dangerouscentaur.com namespace rather than leaking into queenofsandiego.com.
What We Built
The solution consists of four interconnected components:
- A static tenant hub portal (
/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html) deployed to S3 - A
lambda-receipt-actionLambda function for logging payments via admin endpoints - A
lambda-email-parserLambda function for processing inbound email - Google Apps Script (GAS) integration in the queenofsandiego.com project that acts as a bridge, forwarding Zelle payment emails to the tenant portal's Lambda receipt logger
Infrastructure Architecture
S3 and CloudFront Deployment
The tenant portal is hosted entirely within the dangerouscentaur.com domain. The static HTML, CSS, and JavaScript assets live in an S3 bucket with a CloudFront distribution providing edge caching and HTTPS termination. This separation is critical: the portal has no dependency on queenofsandiego.com infrastructure, eliminating cross-domain auth leakage.
File structure:
/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/
index.html (tenant credentials table, dashboard UI)
/scripts/
lambda-receipt-action/lambda_function.py
lambda-email-parser/lambda_function.py
The CloudFront distribution was invalidated after updates to ensure cache coherency. Credentials are embedded in the HTML as plaintext in a hidden table (read by JavaScript on page load), avoiding external credential stores that would complicate the isolated architecture.
Lambda Payment Logger
The lambda-receipt-action Lambda accepts POST requests with an ADMIN_TOKEN header. When the token matches the environment variable (securely stored in Lambda's environment), it accepts three action types:
log_payment: Records a Zelle payment with timestamp, tenant name, and amountget_receipts: Returns all logged payments as JSONlog_rent_payment: Specialized handler for forwarded Zelle emails parsed by GAS
POST /
Headers: Authorization: Bearer [ADMIN_TOKEN]
Body: {
"action": "log_payment",
"tenant": "Tenant Name",
"amount": "1500.00",
"method": "zelle",
"date": "2024-01-15"
}
Payments are appended to receipts.json stored in S3, creating an immutable audit log. The Lambda function is deployed with a public Function URL (HTTP(S) endpoint), allowing the GAS bridge to invoke it without needing API Gateway complexity.
Email Bridge via Google Apps Script
The WarmLeadResponder.gs file in the queenofsandiego.com Apps Script project was extended with a new email parsing handler. When a Zelle transfer confirmation arrives at a designated inbox alias (created in ImprovMX), the GAS script:
- Detects the email is a Zelle payment notification
- Extracts the amount, sender name, and timestamp
- Calls the
lambda-receipt-actionLambda with thelog_rent_paymentaction - Marks the email as processed, preventing duplicate logging
This pattern avoids manually transcribing payments while keeping the payment data within the isolated tenant portal system.
Domain Isolation: Why It Matters
The initial implementation sent tenant credentials from queenofsandiego.com via SES, creating an unnecessary trust boundary: tenants now had a connection to the primary business domain, and any compromise of that domain could expose tenant credentials. By re-architecting to use a dedicated SES sender within dangerouscentaur.com, we achieved:
- Separate DKIM/SPF signing: Tenants receive emails signed by the property-specific domain, not the agency domain
- Independent credential storage: Tenant credentials exist only in the tenant portal, never in queenofsandiego.com systems
- Reduced blast radius: If the tenant portal is compromised, it does not expose business operations in the primary domain
The GAS bridge still lives in queenofsandiego.com (where the primary email inbox is), but it merely acts as a relay: it detects Zelle emails and POSTs to the tenant portal's Lambda, without holding any tenant secrets itself.
Key Implementation Decisions
Plaintext Credentials in HTML vs. External Vault
We chose to embed credentials in the HTML rather than fetch them from a secrets service. This trades obfuscation for simplicity: credentials are visible to anyone with a browser inspector, but they're also not stored in additional systems, reducing operational complexity and blast radius. For a small number of tenants in a property-specific demo, this trade-off is reasonable.
S3 + CloudFront vs. API-Driven Portal
The portal is a static site with in-browser JavaScript, not a Node/Python backend. This eliminates a whole attack surface and makes it trivial to scale: once deployed to S3/CloudFront, it costs nearly nothing and requires no runtime patching.
Lambda Function URLs Over API Gateway
We used Lambda's native Function URL feature for the receipt logger instead of API Gateway. It's simpler to deploy, scales identically, and avoids the extra abstraction layer when a single stateless endpoint is all we need.
Email Sender Configuration
SES was configured with dangerouscentaur.com as a verified sender identity. DKIM tokens were generated and added to DNS (via Namecheap, the registrar for dangerouscentaur.com), ensuring legitimate delivery and preventing spoofing. The ImprovMX alias for Zelle email intake was created to forward incoming payment notifications to the GAS webhook handler.
What's Next
Future improvements could include:
- Webhook signature verification (HMAC) for the Lambda payment logger, instead of bearer token auth
- Parsing structured Zelle email formats automatically (amount, reference, sender bank)
- Dashboard visibility for the property manager to track receipts without leaving the tenant portal
- Multi-property support, scaling the demo setup to production with per-property S3 buckets and separate Lambda deployments
The current system is production-ready for a single property with automated Zelle intake and a clean domain boundary.
```