Building a Domain-Isolated Tenant Portal with Automated Payment Logging via Email Forwarding
Overview
This post documents the architecture and implementation of a self-contained tenant management system built on the dangerouscentaur.com domain, complete with a password-protected hub portal and an automated payment logging system that processes Zelle transfers forwarded via email.
What Was Done
- Created a multi-tenant hub portal at
https://3028fiftyfirststreet.92105.dangerouscentaur.com/ - Generated secure temporary credentials for two tenants and deployed them to the portal
- Configured SES email delivery from a dangerouscentaur.com domain alias
- Built a Lambda-based payment logging system that accepts Zelle confirmation emails
- Integrated Google Apps Script to parse forwarded bank emails and trigger payment logs
- Completely decoupled all infrastructure from queenofsandiego.com
Technical Architecture
Frontend: Tenant Hub Portal
The tenant portal lives in /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html and serves as the single point of access for tenant information. It's a static HTML file hosted on S3 with CloudFront distribution in front, requiring username/password authentication via client-side hashing.
The portal structure includes:
- Credentials table with tenant usernames and password hashes
- Dashboard initialization that loads tenant-specific data sections
- Receipts section that displays logged payments from a JSON manifest
- Admin controls for property managers (hidden behind admin credentials)
Key files modified:
index.html— Portal markup, auth logic, and dashboard structure- S3 bucket:
3028fiftyfirststreet-92105-dangerouscentaur-com - CloudFront Distribution:
dangerouscentaur-demos(ID required for cache invalidation)
Backend: Lambda Functions
Two Lambda functions handle backend operations, both deployed within the dangerouscentaur infrastructure:
- receipt-action — Logs payment records to
receipts.jsonin S3. Located at/scripts/lambda-receipt-action/lambda_function.py, this function accepts admin requests to add payment entries. It validates anADMIN_TOKENenvironment variable before processing writes. - email-parser — Invoked by Google Apps Script when a Zelle confirmation email arrives. Located at
/scripts/lambda-email-parser/lambda_function.py, this function parses the email body for transaction details and calls the receipt-action Lambda to log the payment.
Both functions operate on receipts.json, the source of truth for tenant payment history:
{
"payments": [
{
"tenant": "tenant_name",
"amount": 1500.00,
"date": "2024-04-15",
"method": "zelle",
"reference": "email_message_id"
}
]
}
Email Ingestion: Google Apps Script
A Google Apps Script deployed to the queenofsandiego.com project (file: WarmLeadResponder.gs) acts as the email command handler. When you forward a Zelle confirmation email to a designated inbox, the script:
- Monitors incoming mail for payment-related keywords
- Extracts transaction amount and date from the email body
- Invokes the Lambda email-parser function via HTTPS
- Passes the parsed data as a JSON payload
The critical advantage here: forwarding a bank email is a familiar, native action. No custom uploads, no form submissions—just hit the forward button.
Infrastructure & Deployment
Email Setup: SES + ImprovMX
Initially, credential emails were sent from queenofsandiego.com, which violated the domain isolation requirement. The fix involved:
- Initiating SES domain verification for
dangerouscentaur.com - Retrieving DKIM tokens and publishing them to the domain's DNS records
- Configuring ImprovMX forwarding rules to create an alias inbox (e.g.,
payments@dangerouscentaur.com) that forwards to your personal email - Using the ImprovMX alias as the SES sender identity for all tenant communications
This ensures:
- Tenants receive emails from the property domain, not a personal consulting domain
- Reply-to addresses route back through the property inbox
- No "scary error messages" about SPF/DKIM alignment
S3 & CloudFront
Portal deployment pipeline:
index.html (local)
↓
aws s3 cp index.html s3://3028fiftyfirststreet-92105-dangerouscentaur-com/
↓
aws cloudfront create-invalidation --distribution-id [dist-id] --paths "/*"
↓
https://3028fiftyfirststreet.92105.dangerouscentaur.com/ (live)
Receipts data is stored in the same S3 bucket as receipts.json, allowing Lambda functions to read/write payment history and the portal frontend to load it via CORS-enabled GET requests.
Key Architectural Decisions
Why Client-Side Authentication?
The tenant hub uses client-side password hashing rather than a backend session system. This keeps the infrastructure minimal—no authentication service to maintain, no session tokens to rotate. Passwords are hashed with SHA-256 in the browser before comparison with stored hashes in the HTML. For a small property management context with low traffic, this trades some security depth for operational simplicity.
Why Email Forwarding for Payments?
A dedicated Lambda endpoint for payment logging exists, but manual HTTP requests are friction. Email forwarding is frictionless: your bank already sends you the confirmation, you're already forwarding receipts to accounting—this just replaces "accounting folder" with "Lambda trigger." The Apps Script handles the parsing, so you never touch the Lambda URL.
Why Separate the Domains?
Coupling tenant communications to queenofsandiego.com created brand confusion and violated the principle of least privilege. A property in San Diego should communicate from its own domain. This also allows future properties to have isolated email identities without cross-contamination of DNS or SES configuration.
Deployment Steps & Commands
Generate tenant credentials:
python3 -c "
import secrets, hashlib
for i in range(2):
pwd = secrets.token_urlsafe(12)
hsh = hashlib.sha256(pwd.encode()).hexdigest()
print(f'Tenant {i}: {pwd} → {hsh}')
"