How users add their custom domain dynamically : with fly.io

How users add their custom domain dynamically : with fly.io

Consider you have a web application where users need a feature to connect their own custom domain, and our hosting solution should resolve the site automatically without any manual intervention. One of my recent projects encountered this particular use case. Although there are many guides available for different platforms and frameworks, it took me a while to connect all the dots and build something that works for me. I wanted to share it here in case it helps someone facing the same situation.

I'll be using fly.io to host the complete project, and this guide will provide an overview of the architecture on how to leverage fly.io's GraphQL API to achieve a reliable solution, rather than focusing on coding specifics. The end result would look something like this.

Yeah, enough build up 😌, let's get into it.


To connect any custom domain to fly.io, it involves steps:

  1. Deploying your app to fly.io (obviously)

  2. Create fly certificate from the fly.io dashboard

  3. Updating the domain provider with A, AAAA, CNAME as mentioned in add certificate page.

  4. Waiting till all DNS records changes are updated.

A, AAAA records from the certificate page are important, because it shows us the IPv4 and IPv6 address of our server (aka machines in fly) where the app is hosted.

If you want the full deets on the certificate from fly, go here.

Now we need a way to follow the above steps automatically, so that our users won't be bothered much about the DNS stuff.

A high level view would look something like this:

Fly provides their GraphQl API, which we can leverage to check the configurations. To use the grapnel API you need a API key, which you can get through fly dashboard or cli.

When the user enters the domain, first step is to check for existing certificates for that domain, you can check it using the following GraphQL query:

query($appName: String!, $hostname: String!) {
            app(name: $appName) {
                certificate(hostname: $hostname) {
                    check
                    configured
                    acmeDnsConfigured
                    acmeAlpnConfigured
                    certificateAuthority
                    createdAt
                    dnsProvider
                    dnsValidationInstructions
                    dnsValidationHostname
                    dnsValidationTarget
                    hostname
                    id
                    source
                    clientStatus
                    issued {
                        nodes {
                            type
                            expiresAt
                        }
                    }
                }
            }
        }

Then we are considering the case where, there isn't existing certificate. In that case a new cert is created with the following query:

mutation($appId: ID!, $hostname: String!) {
            addCertificate(appId: $appId, hostname: $hostname) {
                certificate {
                    configured
                    acmeDnsConfigured
                    acmeAlpnConfigured
                    certificateAuthority
                    certificateRequestedAt
                    dnsProvider
                    dnsValidationInstructions
                    dnsValidationHostname
                    dnsValidationTarget
                    hostname
                    id
                    source
                }
            }
        }

That covers the creation part, once the certificate is created successfully, we need to inform our user, that what all DNS records they should update. And this part is something that user has to do by themself, since we don't have any access to their DNS providers. The records that user should update:

A & AAAA records : the value will be the IPv4 and IPv6 for the fly app that we can retrieve with the flyctl command line tool.

CNAME record : This record is an optional from fly that they use for DNS security validation, but I have came across scenarios, that this record is essential to properly resolve the domain. The above mutation query will return this value in the response. The response would look like:

Checking if its connected

Once we prompt the user to update the records, we need to periodically check if these changes are in place. Usually DNS providers takes some time to reflect the changes. So we should have the domain address persisted, and whenever the user comeback to the status page, they can see the connection status.

To check for the changes in individual DNS record, I'm using NodeJs DNS, which will return the domain DNS records directly, can use to check each record invidually. A nodeJs code would look something like:

const aRecords = await DNS.promises.resolve(domain, 'A') ?? [];
if(aRecords.includes(process.env.SERVER_IPV4)){
   status.A = 'ACTIVE'
} else {
   status.A = 'PENDING'
}

Similarly 'AAAA' and 'CNAME' can be checked.

Check the fly certificate status: Fly certificate status indicates if the application is now able to resolve the domain or not. The same query that we used above to get fly.io certificate.

Among the highlighted keys,

IF (clientStatus === 'Ready') or (acmeDnsConfigured === true AND acmeAlpnConfigued === true) THEN the connection is Ready.

Once every check is ready, we can inform the user that their custom domain is ready and all set to use.

Conclusion

Now you have an idea on how approach this similar use case. In addition fly graphQL gives few other endpoints for certs, so you can delete a cert etc. You can extend these to meet your requirement. To resolve the domain address from the application level, I'm using NextJs - Platforms (the repo), which has a pre-built setup of multiple domain resolutions.

If any part is unclear or if you'd like to say hi, please put it in the comments—I'd love to hear from you all!

Happy Coding ✨