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.com with 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:

  1. Tenant Action: Tenant forwards Zelle receipt email to a dangerouscentaur.com alias
  2. Email Parser Lambda: Receives email via SNS/SES, extracts payment metadata (amount, date, payer)
  3. Receipt Action Lambda: Accepts authenticated requests to log payments; updates receipts.json in S3
  4. 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 script
  • RECEIPTS_BUCKET: S3 bucket where receipts.json is stored
  • TENANT_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 3028 property