---
title: "GoToSocial split-domain redirects for a static website on AWS CloudFront"
date: 2025-10-25T13:19:27-07:00
url: https://blog.lmorchard.com/2025/10/25/gotosocial-cloudfront-redirect/
author: Les Orchard
tags: [gotosocial, social, aws, webdev]
---

# GoToSocial split-domain redirects for a static website on AWS CloudFront

For awhile now, I've wanted to set up an installation of [GoToSocial](https://gotosocial.org/) for my [lmorchard.com](https://lmorchard.com) domain and run my own tiny fediverse outpost. And what I *really* wanted to do was to be able to host handles like [@links@lmorchard.com](https://gts.lmorchard.com/@links), [@blog@lmorchard.com](https://gts.lmorchard.com/@blog), and [@lmorchard@lmorchard.com](https://gts.lmorchard.com/@lmorchard). ([I was thinking of doing @me@lmorchard.com](https://masto.hackers.town/@lmorchard/115413464705080292), just like my email address. But, that could be confusing, because I might look like my name is "me" everywhere.)

Per the GoToSocial documentation, [Split-domain deployments](https://docs.gotosocial.org/en/latest/advanced/host-account-domain/) are supported by way of a few server-side redirects on the vanity account domain:

> The way ActivityPub implementations discover how to map your account domain to your host domain is through a protocol called [webfinger](https://www.rfc-editor.org/rfc/rfc7033). This mapping is typically cached by servers and hence why you can't change it after the fact.
>
> It works by doing a request to `https://<account domain>/.well-known/webfinger?resource=acct:@me@example.org`. At this point, a server can return a redirect to where the actual webfinger endpoint is, `https://<host domain>/.well-known/webfinger?resource=acct:@me@example.org` or may respond directly. The JSON document that is returned informs you what the endpoint to query is for the user

So, I need `lmorchard.com/.well-known/webfinger` to redirect to `gts.lmorchard.com/.well-known/webfinger` with query parameters intact to make the magic happen.

There's a wrinkle, though: lmorchard.com points at a statically-generated site, uploaded to Amazon S3, hosted behind a CloudFront CDN. That's been low-hassle to keep running for years now, as opposed to say a full-featured nginx server. The trade-off has been that this hosting arrangement didn't support any smarts on the server side. So, I thought the redirects would be infeasible.

However, I'd missed that [CloudFront added support for edge functions](https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/) a few years ago. That means redirects are *entirely* feasible these days!

Long story short, here's the edge function I came up with to do the needful for GoToSocial. Nothing super-special, just that a) it works and b) it took me a few rounds of mistakes before I got it working. So, this might be handy for someone else trying to do something similar! (Or me, if I ever lose it and need to set this up again.)

```javascript
function handler(event) {
    var request = event.request;
    var uri = request.uri;
    
    // Check if the request is for one of the well-known endpoints
    if (uri === '/.well-known/webfinger' || 
        uri === '/.well-known/host-meta' || 
        uri === '/.well-known/nodeinfo') {
        
        // Build redirect URL
        var redirectUrl = 'https://gts.lmorchard.com' + uri;
        
        // Manually build query string from querystring object
        var queryString = request.querystring;
        if (queryString && Object.keys(queryString).length > 0) {
            var params = [];
            for (var key in queryString) {
                if (queryString.hasOwnProperty(key)) {
                    var value = queryString[key].value;
                    // Don't re-encode - values are already URL-encoded
                    params.push(key + '=' + value);
                }
            }
            if (params.length > 0) {
                redirectUrl += '?' + params.join('&');
            }
        }
        
        return {
            statusCode: 301,
            statusDescription: 'Moved Permanently',
            headers: {
                'location': { value: redirectUrl },
                'access-control-allow-origin': { value: '*' },
                'access-control-allow-methods': { value: 'GET, HEAD, OPTIONS' },
                'access-control-allow-headers': { value: 'Content-Type' }
            }
        };
    }
    
    // Return the request unchanged for all other paths
    return request;
}
```
