Building a Self-Contained Tenant Payment Portal with Automated Zelle Receipt Logging
We built a complete tenant management system for a rental property that handles credential distribution, payment tracking, and automated receipt ingestion—all while maintaining strict domain isolation from a separate business entity.
The Problem
A property manager needed to:
- Issue secure credentials to tenants for a private portal
- Track rental payments made via Zelle (bank transfers)
- Avoid manual data entry for payment logging
- Keep all infrastructure completely isolated on the
dangerouscentaur.comdomain (not sharing withqueenofsandiego.com)
The critical constraint: domain isolation. Previous email communications from queenofsandiego.com created security and brand concerns, so we rebuilt the entire system within the dangerouscentaur.com namespace.
Architecture Overview
The system consists of three integrated components:
- Frontend: Static HTML tenant hub with credential display and payment receipt viewing
- Lambda APIs: Serverless functions for receipt action logging and email parsing
- Email Ingestion Pipeline: Google Apps Script that forwards Zelle emails to Lambda for automatic payment logging
Tenant Hub Portal
The hub lives at https://3028fiftyfirststreet.92105.dangerouscentaur.com/ and is deployed to an S3 bucket at s3://3028fiftyfirststreet.92105.dangerouscentaur.com/. The file /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html contains:
- A credentials table with generated temporary passwords (refreshed for each tenant)
- A receipts section that dynamically loads payment history from the backend
- JavaScript handlers that call Lambda endpoints to populate receipts and execute admin actions
The portal uses environment-based configuration to reference Lambda function URLs, allowing the same codebase to work across different deployments without hardcoding sensitive URLs.
Why static S3 + CloudFront: S3 provides version control history, CloudFront handles HTTPS/caching, and there's no server overhead. We invalidated the CloudFront distribution (specific distribution ID tracked in deployment scripts) after each update to ensure fresh content.
Credential Generation and Distribution
When tenants pay the security deposit, we:
- Generate fresh temporary passwords for each tenant
- Hash passwords using standard algorithms (stored in the portal's HTML table)
- Update
index.htmlwith new hashes - Redeploy to S3 and invalidate CloudFront cache
- Send credential emails via AWS SES from a verified
dangerouscentaur.comsender address
All SES email sending was done via AWS CLI from the authenticated environment:
aws ses send-email \
--from "noreply@dangerouscentaur.com" \
--to [tenant-email] \
--subject "Your Tenant Portal Credentials" \
--text "Visit: https://3028fiftyfirststreet.92105.dangerouscentaur.com/"
Why verified sender domain: Previous emails from queenofsandiego.com raised concerns about domain legitimacy and brand confusion. By establishing dangerouscentaur.com as the authoritative sender (with DKIM/SPF records configured), we ensure email deliverability and clarity about which entity is communicating.
Lambda Functions for Receipt Management
Two Lambda functions handle the backend logic:
Receipt Action Logger
File: /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-receipt-action/lambda_function.py
This function provides an HTTP endpoint (via Lambda function URL) that accepts admin actions, including a new log_rent_payment action:
def log_rent_payment(tenant_id, amount, payment_date, method="zelle"):
"""Log a rent payment to receipts.json in S3"""
receipt = {
"tenant_id": tenant_id,
"amount": float(amount),
"date": payment_date,
"method": method,
"logged_at": datetime.now().isoformat()
}
# Append to receipts.json in S3
return receipt
The function validates an ADMIN_TOKEN environment variable before executing any action, preventing unauthorized payment logging. The token is set during Lambda deployment and stored in the runtime environment.
Email Parser Lambda
File: /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-email-parser/lambda_function.py
This function receives forwarded emails from the Google Apps Script pipeline and extracts payment details from Zelle email formats. It then calls the receipt-action Lambda to log the payment.
Google Apps Script Email Forwarding
File: /Users/cb/Documents/repos/sites/queenofsandiego.com/WarmLeadResponder.gs (note: this GAS project is actually bound to a dangerouscentaur.com-owned Google Workspace account, not queenofsandiego.com—naming is legacy)
The script implements an email handler that:
- Monitors an inbox alias on the
dangerouscentaur.comdomain (e.g.,payments@dangerouscentaur.com) - Detects forwarded Zelle confirmation emails based on sender and subject patterns
- Extracts tenant ID, amount, and date from email body/headers
- Calls the Lambda email-parser endpoint with extracted data
The key function in the execute block:
function log_rent_payment(tenantId, amount, paymentDate) {
var payload = {
action: "log_rent_payment",
tenant_id: tenantId,
amount: amount,
payment_date: paymentDate,
admin_token: ADMIN_TOKEN
};
var options = {
method: "post",
payload: JSON.stringify(payload),
contentType: "application/json"
};
var response = UrlFetchApp.fetch(LAMBDA_URL, options);
return JSON.parse(response.getContentText());
}
Why Google Apps Script: GAS is tightly integrated with Gmail and Google Workspace. The property manager forwards Zelle emails to a GAS-monitored inbox, and the script automatically parses and logs payments without manual intervention. This beats separate IFTTT or Zapier services and keeps everything in one ecosystem.