Building a Self-Contained Tenant Portal with Email-Driven Payment Logging
What Was Done
We built a complete tenant management system for a rental property, divorcing it entirely from an existing domain and establishing it within a new dangerouscentaur.com namespace. The system includes:
- A tenant hub portal at
3028fiftyfirststreet.92105.dangerouscentaur.comwith credential management - AWS Lambda functions for receipt logging and email parsing
- An email-forwarding pipeline that auto-logs Zelle payments when tenants forward bank receipts
- SES domain verification and alias setup for dangerouscentaur.com
- CloudFront distribution invalidation and S3 deployment
Technical Details
Portal Infrastructure
The tenant hub lives at /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/ with this structure:
index.html # Portal UI with embedded credentials table
scripts/
├── lambda-receipt-action/
│ └── lambda_function.py # Handles payment logging actions
└── lambda-email-parser/
└── lambda_function.py # Parses forwarded emails for payment data
The index.html serves as the main tenant interface. It contains a credentials table (generated fresh during deployment) and a receipts section that dynamically loads from a JSON manifest stored in S3. Tenants authenticate via Lambda function URLs protected by an admin token passed in the request header.
Payment Logging Architecture
Rather than requiring manual entry, we implemented an email-forwarding pattern:
- Tenant Action: Tenant forwards Zelle receipt email to a dangerouscentaur.com alias
- Email Parser Lambda: Receives email via SNS/SES, extracts payment metadata (amount, date, payer)
- Receipt Action Lambda: Accepts authenticated requests to log payments; updates
receipts.jsonin S3 - Portal Refresh: Hub portal polls receipts endpoint and displays logged payments in real-time
The receipt-action Lambda (lambda_function.py) exposes a function URL at:
https://<lambda-id>.lambda-url.<region>.on.aws/
It requires an Authorization header with an admin token (stored in Lambda environment variables) and accepts JSON payloads with keys: tenant_id, amount, payment_date, method (e.g., "zelle"), and receipt_reference.
GAS Command Handler Integration
Google Apps Script in WarmLeadResponder.gs was extended with a payment logging handler. When a forwarded email arrives, the GAS script:
- Detects Zelle receipt patterns in email subject/body
- Extracts sender, amount, and transaction timestamp
- Maps the email address to the correct
tenant_id - Makes an authenticated POST to the receipt-action Lambda with the payment data
Infrastructure Changes
SES Domain Verification
To enable outbound email from dangerouscentaur.com, we initiated domain verification with AWS SES. The process generates DKIM tokens that must be added to your DNS provider (Namecheap):
# Command (no credentials shown):
aws ses verify-domain-dkim --domain dangerouscentaur.com --region us-west-2
The returned DKIM tokens create CNAME records in DNS. Once verified, SES can send from any address @dangerouscentaur.com.
Email Alias Setup
For inbound receipt emails, we configured an alias (e.g., receipts@dangerouscentaur.com) that routes to the email-parser Lambda via SNS. This is configured in SES Email Receiving rules.
S3 & CloudFront Deployment
The updated index.html was deployed to S3 at:
s3://<bucket>/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html
CloudFront distribution cache was invalidated using:
aws cloudfront create-invalidation \
--distribution-id <dist-id> \
--paths "/*" \
--region us-west-2
This ensures tenants immediately see the new credentials and portal updates.
Lambda Environment Configuration
The receipt-action Lambda was configured with environment variable:
ADMIN_TOKEN: Used to validate incoming requests from the email-parser Lambda and GAS scriptRECEIPTS_BUCKET: S3 bucket wherereceipts.jsonis storedTENANT_ID: Property identifier (3028) for S3 path namespacing
Key Decisions
Complete Domain Isolation
By moving all tenant-facing systems to dangerouscentaur.com, we eliminated cross-domain email issues and avoid SPF/DKIM complications that arise from sending tenant credentials through queenofsandiego.com. This creates a cleaner mental model: all rental operations live in one domain.
Email-First Payment Workflow
Rather than building a custom payment form, we leveraged the existing email infrastructure. Tenants already receive bank notifications; forwarding them to a service email is a lower-friction UX than logging into a portal to manually record payments. The GAS script acts as the glue layer, parsing emails and posting to Lambda.
Stateless Lambda Functions
Both Lambdas are stateless and read/write directly to S3 (receipts.json). This avoids database setup, keeps billing minimal, and makes the system easier to audit—all payment records are versioned in S3.
Admin Token Pattern
Lambda function URLs are publicly accessible, but all payment endpoints require an admin token in the Authorization header. This prevents unauthorized payment logging while keeping the API simple.
What's Next
Future enhancements could include:
- Receipt Image Storage: Extend the email parser to extract and store receipt images in S3, linked from the receipts table
- Rent Ledger View: Add a tenant-accessible page showing payment history, outstanding balances, and late-payment alerts
- Automated Rent Tracking: Create a CloudWatch rule to check the receipts ledger daily and send alerts if rent is overdue
- Multi-Property Scaling: Abstract the
3028property