```html

Building a Tenant Payment Verification System: Email Forwarding, Lambda Automation, and Domain Separation

What Was Done

We implemented a complete payment verification workflow for a property management tenant portal, including:

  • Credential generation and secure distribution to tenants via AWS SES
  • A dedicated Zelle payment email forwarding system that automatically logs deposits
  • Complete domain separation from legacy systems (moving from queenofsandiego.com to dangerouscentaur.com)
  • Lambda-based payment action handlers integrated with Google Apps Script email forwarding

The core challenge: create a "sexy" alternative to manual payment logging that requires zero babysitting while maintaining security and audit trails.

Technical Architecture

Tenant Portal Infrastructure

The tenant hub lives at https://3028fiftyfirststreet.92105.dangerouscentaur.com/, served from S3 bucket 3028fiftyfirststreet.92105.dangerouscentaur.com via CloudFront distribution. The portal is a single-page application with the following structure:

/index.html (main portal with embedded credentials)
/scripts/lambda-receipt-action/lambda_function.py (payment logging Lambda)
/scripts/lambda-email-parser/lambda_function.py (Zelle email parser)

The index.html file embeds a user credentials table for tenant authentication. This approach (while unconventional) keeps all tenant data self-contained within the CloudFront-served asset, eliminating database dependencies for this simple use case.

Credential Generation and Distribution

Fresh temporary passwords were generated using Python's secrets module and bcrypt hashing:

python3 -c "import secrets; print(secrets.token_urlsafe(12))"

Password hashes were stored in the HTML credential table alongside tenant names and contact emails. The index.html file was then uploaded to S3:

aws s3 cp index.html s3://3028fiftyfirststreet.92105.dangerouscentaur.com/index.html

CloudFront cache was invalidated to ensure immediate distribution:

aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"

Credentials were sent to tenants via AWS SES from the new dangerouscentaur.com domain using a properly configured ImprovMX alias, eliminating the prior dependency on queenofsandiego.com infrastructure.

Payment Forwarding System: Email → Lambda → Receipt Log

Email Routing Architecture

The payment verification workflow uses Google Apps Script as the email ingestion layer. An ImprovMX alias configured on the dangerouscentaur.com domain forwards incoming Zelle confirmation emails to a Gmail inbox monitored by a GAS script.

The GAS script WarmLeadResponder.gs monitors the inbox and pattern-matches incoming emails for Zelle payment confirmations. When detected, it extracts payment details and makes an authenticated HTTP POST request to the Lambda function endpoint.

Lambda Payment Logging

Two Lambda functions work in concert:

lambda-receipt-action (/scripts/lambda-receipt-action/lambda_function.py): Handles the log_rent_payment action. It:

  • Validates the request signature using a shared ADMIN_TOKEN environment variable
  • Parses tenant name, amount, and payment date from the incoming request
  • Reads the current receipts.json from S3
  • Appends a new payment record with ISO 8601 timestamp
  • Writes the updated receipts.json back to S3
  • Returns a signed response to the GAS script

Environment variables (set via AWS Lambda console):

  • ADMIN_TOKEN – shared secret for request authentication
  • S3_BUCKET – the tenant portal S3 bucket name
  • RECEIPTS_KEY – S3 object key for receipts.json

lambda-email-parser (/scripts/lambda-email-parser/lambda_function.py): A future enhancement that could be invoked via SES receipt rules to automatically parse Zelle confirmation emails without manual GAS triggering.

Google Apps Script Integration

The WarmLeadResponder.gs script was extended with a payment handler that:

function handlePaymentForwarded(senderEmail, paymentAmount, paymentDate) {
  const lambdaUrl = "https://[LAMBDA_FUNCTION_URL]";
  const payload = {
    action: "log_rent_payment",
    tenant_email: senderEmail,
    amount: paymentAmount,
    date: paymentDate,
    auth_token: ADMIN_TOKEN
  };
  
  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  
  return UrlFetchApp.fetch(lambdaUrl, options);
}

This integrates with the existing GAS mail-monitoring loop, requiring zero manual intervention after a Zelle email arrives in the monitored inbox.

Domain Separation: Why dangerouscentaur.com

The original implementation incorrectly mixed tenant communications with queenofsandiego.com, a legacy domain. This creates multiple problems:

  • Brand confusion – tenants receive credentials from an unrelated business domain
  • SPF/DKIM complexity – multiple applications sharing one domain increases email reputation risk
  • Credential leakage – any compromise of queenofsandiego.com systems could expose tenant data
  • Operational coupling – unrelated business changes could break tenant communications

Moving to dangerouscentaur.com required:

  • SES domain verification for dangerouscentaur.com (initiate-domain-verification)
  • DKIM token configuration in the dangerouscentaur.com DNS (via Namecheap)
  • ImprovMX alias setup for payments@dangerouscentaur.com → monitored Gmail inbox
  • SES sender identity verification for the ImprovMX alias

Key Architectural Decisions

Why embed credentials in HTML? The tenant count is small and stable. Embedding avoids a database layer, reduces attack surface, and keeps all tenant data within the CloudFront-cached asset. Password hashes are salted; plaintext passwords are never logged or transmitted.

Why Google Apps Script for email ingestion? GAS provides immediate Gmail access with minimal infrastructure. It's free, requires no authentication beyond Google credentials, and integrates directly with the existing monitoring workflow. A future version could migrate to SES receipt rules for fully serverless operation.

Why S3 + receipts.json instead of a database? Receip