Building a Domain-Isolated Tenant Payment Portal with Lambda-Backed Email Forwarding

What Was Done

We built a complete tenant management portal at 3028fiftyfirststreet.92105.dangerouscentaur.com with these core features:

  • Tenant credential distribution via SES from a dedicated dangerouscentaur.com domain inbox
  • A receipt logging system that accepts Zelle payment confirmations forwarded via email
  • Automatic parsing and logging of payment emails into a centralized receipts ledger
  • Complete domain isolation from queenofsandiego.com to prevent cross-domain confusion and reputation issues

The core problem: sending tenant credentials through a domain they don't recognize (queenofsandiego.com) creates trust issues and operational friction. The solution: build a self-contained payment and credential workflow entirely within the dangerouscentaur.com namespace, backed by Lambda functions and Google Apps Script handlers.

Technical Architecture

Frontend: The Tenant Hub

The tenant portal lives at /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html and serves as a single-page dashboard. Key sections include:

  • Credentials display: Shows tenant login credentials generated with SHA-256 hashes (passwords stored client-side only for initial setup)
  • Receipts section: Dynamically loads from a centralized receipts.json file stored in S3
  • Payment logging interface: Form UI that triggers backend Lambda functions

The hub is deployed to S3 at a bucket managed by dangerouscentaur infrastructure and served through CloudFront for caching and HTTPS termination.

Backend: Lambda Functions

Two Lambda functions handle the payment workflow:

  • lambda-receipt-action (/scripts/lambda-receipt-action/lambda_function.py): Receives HTTP POST requests with payment details. Validates requests using an ADMIN_TOKEN environment variable. Appends new payment records to receipts.json in S3.
  • lambda-email-parser (/scripts/lambda-email-parser/lambda_function.py): New function created to parse forwarded Zelle emails. Extracts payment amount, sender, and timestamp. Calls the receipt-action Lambda with admin credentials to log the payment.

Both functions are deployed as standalone Lambda functions with function URLs, allowing direct HTTPS invocation without API Gateway overhead. The receipt-action Lambda is configured with:

  • Resource-based policy allowing SES to invoke it
  • ADMIN_TOKEN environment variable for internal authentication
  • S3 permissions to read/write receipts.json

Email Forwarding Pipeline: Google Apps Script

The WarmLeadResponder.gs script (in the queenofsandiego.com repo, but with new tenant-specific handlers) has been extended to handle the 3028 property workflow:

  • Email trigger: When an email arrives at a monitored inbox alias (e.g., payments@dangerouscentaur.com), GAS captures it
  • Zelle detection: Script regex-matches Zelle payment confirmations
  • Lambda invocation: On match, calls the lambda-email-parser function with the forwarded email body
  • Logging: Payment gets recorded in receipts.json and visible on the tenant hub within minutes

This creates a fully automated pipeline: tenant sends Zelle → bank sends confirmation email to your mail account → you forward to payments@dangerouscentaur.com → GAS parses it → Lambda logs it → tenant sees it in the hub.

Infrastructure Details

Domain and Email Setup

Critical decision: use a dedicated dangerouscentaur.com SES sender identity instead of queenofsandiego.com. This requires:

  • SES domain verification: DKIM tokens added to Namecheap DNS for dangerouscentaur.com
  • ImprovMX aliases: Created inbox aliases like payments@dangerouscentaur.com that forward to your personal mailbox
  • SPF/DKIM records: Proper DNS configuration prevents emails from landing in spam

Command to verify domain in SES:

aws ses verify-domain-identity --domain dangerouscentaur.com --region us-west-2

Then retrieve DKIM tokens and add them to your DNS provider (Namecheap in this case).

S3 and CloudFront

The tenant hub index.html and receipts.json are stored in an S3 bucket. CloudFront distribution caches the static content with invalidation on updates:

  • S3 bucket: Houses both index.html and receipts.json
  • CloudFront distribution ID: Cached for quick invalidation after deploys
  • Invalidation pattern: After updating files, invalidate /* to clear all cached objects

Example invalidation command:

aws cloudfront create-invalidation --distribution-id DISTRIBUTION_ID --paths "/*" --region us-west-2

Key Decisions and Rationale

Why Lambda over API Gateway? Function URLs are simpler, cheaper, and don't require managing API Gateway resources. Since the receipt-action endpoint is internal-only (protected by ADMIN_TOKEN), we don't need Gateway's request validation or rate limiting features.

Why Apps Script for email parsing? GAS integrates natively with Gmail and Google Sheets, making it trivial to monitor an inbox and trigger webhooks. It's also free and requires minimal operational overhead compared to an SNS-to-SQS-to-Lambda pipeline.

Why separate from queenofsandiego.com? Domain reputation and SPF/DKIM records are sender-specific. Using queenofsandiego.com for dangerouscentaur property emails created brand confusion and mail deliverability risk. Tenants receiving mail from an unrelated real estate domain raises red flags. Separation ensures each property/domain can manage its own email reputation independently.

Why receipts.json instead of a database? For small volumes (a few payments per month per property), a JSON file is simpler and cheaper than running DynamoDB or RDS. S3 versioning provides audit trails, and CloudFront caching keeps read latency low. As volume grows, migration to DynamoDB is straightforward.

Deployment Process

Updated hub HTML deployed to S3:

aws s3 cp index.html s3://bucket-name