Skip to content

Add CloudFront OG shell infrastructure#11587

Draft
nbudin wants to merge 8 commits into
mainfrom
cloudfront-og-shell
Draft

Add CloudFront OG shell infrastructure#11587
nbudin wants to merge 8 commits into
mainfrom
cloudfront-og-shell

Conversation

@nbudin
Copy link
Copy Markdown
Contributor

@nbudin nbudin commented Jun 1, 2026

Purpose

The SPA controller currently serves per-resource Open Graph metadata (event titles, page descriptions) by doing CMS Liquid rendering and DB lookups on every request. The goal is to eventually serve a fully static HTML shell for SPA routes, but we can't do that without some way to still give crawlers correct OG tags.

This PR adds the infrastructure to make that work with CloudFront. The existing single_page_app#root behavior is completely unchanged — direct access to the Rails app still serves full per-resource OG tags as before. CloudFront is optional, not required.

Changes

💻 New Rails endpoints

  • GET /og-shell?path=<path> — minimal bare HTML (no layout) with per-resource OG tags. Looks up the event or CMS page from the path, pulls cached_og_description for pages, uses short_blurb for events. This is what crawlers get through CloudFront.
  • GET /cdn-spa-shell — lightweight SPA shell with convention-level OG only (name, image, favicon). No @page/@event DB lookups. This is what regular CloudFront users get instead of hitting the full SPA controller.

💻 Lambda@Edge function (lambda/cloudfront-og-router/)

Origin-request handler that rewrites based on User-Agent: known crawlers go to /og-shell?path=<original-path>, everyone else goes to /cdn-spa-shell. ~30 lines of JS, no dependencies.

💻 Terraform module (terraform/modules/cloudfront_intercode/)

Reusable module for the full distribution. Handles the assets origin (/packs/* with long-TTL immutable caching), pass-through behaviors for API/auth endpoints, cache policies keyed on Host header (important for multi-tenant convention routing), and the Lambda@Edge wiring on the default behavior.

Risks

The Host header forwarding is the trickiest part — Intercode resolves conventions by domain, so CloudFront needs to forward the viewer's Host to the Rails origin rather than substituting the origin hostname. The origin request policies in the module handle this, but it's worth verifying in staging before pointing any real convention domains at it.

Release plan and notes

Nothing to ship yet — this is groundwork. The next step is wiring up an actual CloudFront distribution using the module and pointing a convention domain at it to validate the setup.

🤖 Generated with Claude Code

nbudin and others added 8 commits June 1, 2026 13:57
Adds two new Rails endpoints and supporting infrastructure to serve
per-resource Open Graph metadata to crawlers through CloudFront, while
keeping the existing single_page_app#root intact for direct access.

Rails:
- GET /og-shell?path=<path> — minimal HTML with per-resource OG tags
  (event title/blurb, page cached_og_description, convention OG image).
  Intended for crawlers via Lambda@Edge rewrite.
- GET /cdn-spa-shell — lightweight SPA shell with convention-level OG
  only, no @page/@event DB lookups. CloudFront serves this to regular
  users instead of hitting the full SPA controller.

Lambda@Edge (lambda/cloudfront-og-router/):
- Origin-request handler that rewrites crawler User-Agents to /og-shell
  and everyone else to /cdn-spa-shell.

Terraform (terraform/modules/cloudfront_intercode/):
- Reusable module for the full CloudFront distribution: assets origin
  (/packs/*), Rails origin, ordered cache behaviors, Lambda@Edge wiring,
  Host-header forwarding for multi-tenant convention routing.

Direct access to the Rails app (without CloudFront) continues to work
unchanged — single_page_app#root still serves full per-resource OG tags.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds missing pass-through CloudFront behaviours for: /users/*, /oauth/*,
/reports/*, /calendars/*, /csv_exports/*, /user_con_profiles/*,
/stripe_account/*, /email_forwarders/*, /sitemap.xml.

Also adds a RAILS_PATH_RE bypass in the Lambda@Edge function as a second
line of defence so requests to real Rails routes are never accidentally
rewritten to /cdn-spa-shell or /og-shell if a new route is added without
a corresponding CloudFront behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts the SQS queues, S3 uploads bucket, and IAM user/group/policy
from neil-terraform into a reusable module alongside the existing
cloudfront_intercode module.

Creates: default/mailers/ahoy/dead-letter SQS queues, S3 bucket with
CORS config, IAM group+user+access key with policy covering SQS, S3,
SES, CloudWatch scheduler provisioning, and optional SNS/KMS access.
Also creates a CloudWatch alarm that fires when the oldest message in
any non-DLQ queue exceeds 10 minutes.

inbox_bucket_arn, inbox_sns_topic_arn, and kms_key_arn are optional
variables for resources defined outside the module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pulls in ses-receiving.tf from neil-terraform: inbox S3 bucket with
14-day lifecycle, SES receipt rule set + active rule, SNS topic for
delivery notifications with HTTPS webhook subscription, SNS feedback
IAM roles, and a SES configuration set with CloudWatch event tracking.

Since the inbox bucket and SNS topic are now created internally:
- Removes inbox_bucket_arn, inbox_sns_topic_arn, kms_key_arn variables
- Adds inbox_bucket_name and sns_notification_endpoint variables
- IAM policy references internal resources directly; KMS access uses
  the AWS-managed alias/aws/ses key instead of a hardcoded key ID

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves the SES inbox bucket, receipt rules, SNS topic + webhook
subscription, and SNS feedback IAM roles out of intercode_aws_resources
and into a new ses_email_receiving module.

intercode_aws_resources is restored to accepting optional
inbox_bucket_arn, inbox_sns_topic_arn, and kms_key_arn variables, so it
works with ses_email_receiving or any other email handling module via
output wiring.

ses_email_receiving exposes kms_key_arn as an output (the AWS-managed
alias/aws/ses key) so callers can feed it straight into
intercode_aws_resources without hardcoding key IDs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ses_email_receiving now takes iam_group_name as a variable and attaches
its own aws_iam_group_policy covering S3 inbox access, sns:ConfirmSubscription,
and kms:Decrypt (alias/aws/ses) directly to the app's IAM group.

intercode_aws_resources no longer needs inbox_bucket_arn,
inbox_sns_topic_arn, or kms_key_arn variables. It exposes iam_group_name
as an output so any email module can self-attach its policy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Data-only module that fetches domain verification records from the
forwardemail.net API (paginated). Outputs a domain => verification_code
map for use with forwardemail_receiving_domain DNS modules.

No AWS resources or IAM policies — forwardemail receiving is
DNS-only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 1, 2026

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
config/routes.rb 🟢 88.24% 🟢 88.68% 🟢 0.44%
Overall Coverage 🟢 52.81% 🟢 52.82% 🟢 0.01%

Minimum allowed coverage is 0%, this run produced 52.82%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant