```html

Building a Tenant Payment Portal with Email-Forwarding Integration: Separating Concerns Across AWS and Google Apps Script

What Was Done

We built a complete tenant management system that allows property managers to:

  • Deploy a secure tenant hub portal at a dedicated domain
  • Issue temporary credentials to tenants via verified SES email
  • Log rent payments by forwarding Zelle notifications to an email handler
  • Automatically parse payment emails and update a centralized receipt ledger
  • Maintain strict domain separation (all operations within dangerouscentaur.com, completely divorced from queenofsandiego.com)

The system integrates three core components: a static tenant portal hosted on S3/CloudFront, AWS Lambda functions for payment processing, and Google Apps Script for email command parsing.

Technical Architecture

Tenant Portal Infrastructure

The tenant hub is deployed to s3://dc-sites-tenanthub/ and distributed via CloudFront (distribution ID varies by environment). The portal lives at 3028fiftyfirststreet.92105.dangerouscentaur.com—a property-specific subdomain that allows scaling to multiple properties under the same infrastructure.

The index.html file contains:

  • User credential table (name, email, temporary password)
  • Dashboard section with receipt history and payment status
  • JavaScript that loads receipt data dynamically from the Lambda endpoint

Credentials are generated as follows:

import hashlib
import secrets

# Generate 12-character alphanumeric temporary password
temp_password = secrets.token_urlsafe(9)[:12]

# Create bcrypt-like hash for storage (simplified example)
password_hash = hashlib.pbkdf2_hmac(
    'sha256',
    temp_password.encode(),
    b'portal-salt-value',
    100000
).hex()

Payment Logging Lambda

Two Lambda functions handle payment workflows:

lambda-receipt-action (/scripts/lambda-receipt-action/lambda_function.py):

  • Exposes a public HTTP endpoint via Lambda Function URL
  • Accepts POST requests with payment data (tenant ID, amount, payment method, date)
  • Validates requests using an ADMIN_TOKEN environment variable
  • Appends structured records to receipts.json in S3
  • Triggers CloudFront invalidation to refresh cached receipt data

lambda-email-parser (/scripts/lambda-email-parser/lambda_function.py):

  • Triggered by SES receipt notifications
  • Parses forwarded Zelle emails for sender, amount, and date
  • Invokes the receipt-action endpoint with extracted payment details
  • Handles parsing errors gracefully without breaking the payment pipeline

Both Lambdas are deployed with:

cd /Users/cb/Documents/repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/scripts/lambda-receipt-action
zip -r lambda_function.zip lambda_function.py
aws lambda update-function-code \
  --function-name log-receipt-action \
  --zip-file fileb://lambda_function.zip

SES Domain Setup

Rather than sending credentials from queenofsandiego.com (which caused domain trust issues), we configured SES to send from dangerouscentaur.com:

  • Verified dangerouscentaur.com as a sending domain in SES
  • Retrieved DKIM tokens via AWS CLI and added them to Namecheap DNS
  • Configured an ImprovMX alias to forward tenant inquiry emails to a management inbox
  • Set noreply@dangerouscentaur.com as the credential email sender

Tenant credential emails are sent via:

aws ses send-email \
  --from noreply@dangerouscentaur.com \
  --to tenant-email@example.com \
  --subject "Your Tenant Portal Credentials" \
  --text "Portal: https://3028fiftyfirststreet.92105.dangerouscentaur.com\nUsername: [tenant-name]\nPassword: [temp-password]"

Email-to-Payment Pipeline via Google Apps Script

Google Apps Script acts as the bridge between email forwarding and payment logging. When a tenant forwards a Zelle confirmation to payments@dangerouscentaur.com (via ImprovMX), the GAS script:

  • Polls Gmail for new messages with subject line patterns matching Zelle notifications
  • Extracts sender name, payment amount, and transaction timestamp
  • Calls the Lambda receipt-action endpoint with an admin token
  • Marks processed emails as read to avoid duplicates

The handler in WarmLeadResponder.gs includes:

function handlePaymentCommand(emailData) {
  const adminToken = PropertiesService.getScriptProperties().getProperty('ADMIN_TOKEN');
  const payload = {
    tenant_id: emailData.tenantId,
    amount: emailData.amount,
    payment_method: 'zelle',
    date: emailData.date
  };
  
  const options = {
    method: 'post',
    payload: JSON.stringify(payload),
    headers: { 'Authorization': `Bearer ${adminToken}` },
    muteHttpExceptions: true
  };
  
  UrlFetchApp.fetch(LAMBDA_RECEIPT_ENDPOINT, options);
}

Key Design Decisions

Domain Separation: By using dangerouscentaur.com exclusively, we avoid email deliverability issues and ensure the system is self-contained. A separate property would get its own subdomain (e.g., 4567oak.92103.dangerouscentaur.com).

Temporary Passwords: Rather than generating permanent credentials, tenants receive time-limited passwords. This encourages them to set their own passwords on first login and reduces the risk of exposed credentials remaining valid indefinitely.

Email Forwarding Over Manual Entry: Asking tenants to forward Zelle notifications is far less friction than asking them to log into a portal and enter payment details. The property manager never needs to manually type payment data.

Centralized Receipt Ledger: All payments write to a single receipts.json file in S3. This serves as the source of truth and allows the portal to render payment history without hitting a database.

Admin Token Authentication: Lambda endpoints are protected by an environment variable token rather than exposing them publicly. The GAS script stores this token securely in Properties, preventing unauthorized payment logging.

Infrastructure Resources

  • S3 Bucket: dc-sites-tenanth