Approximated Custom Domains and SSL for SAAS Documentation

Guides and API documentation to get you up and running.

How Approximated Works

Approximated helps you connect web domains to your application(s). These are often called custom domains when provided by your users.

This is accomplished by relaying internet traffic to your app(s) and back again for each domain that you configure, using a gloablly distributed set of machines called a Proxy Cluster. This Proxy Cluster also provides and manages SSL certificates for each domain it's responsible for.

What is a Proxy Cluster?

Approximated's Proxy Clusters are groups of machines distributed globally that serve traffic for you. When you create one in Approximated, these machines are set aside and dedicated to you, along with your own dedicated IPv4 address.

You can create Virtual Hosts on your Proxy Cluster to tell it which domains to accept traffic from, and where to send it. You can have as many Proxy Clusters as you need, and each one can scale to as many custom domains as you want.

What is a Virtual Host?

Virtual hosts are entities that you create in Approximated. They tell Approximated how to route requests for custom domains. Each virtual host has an incoming address field that matches the custom domain you want to route. It also has a target address field that tells Approximated where to send requests for that custom domain.

What role does DNS play?

Every custom domain you want to connect needs to be pointed with DNS at your Approximated proxy cluster, so that it can route them for you. This can be done with an A record pointed at your cluster's IPv4 address, or through an intermediary domain/subdomain with a CNAME record.

If a DNS record is pointed at your cluster without a matching virtual host, it will be ignored so that you control what custom domains are routed by your cluster.

Choosing A records or CNAME records
Apex domains
An apex domain is a plain domain without www or any subdomains prepended.
Example apex domains: mydomain.com, google.com
These are not apex domains: www.mydomain.com, docs.google.com
DNS A records
The simplest way to point a domain at your cluster is a DNS A record, but you should be aware of a few things:
  • If you ever choose to change to a different IP address, you will need to have all custom domains change their A records individually.
  • A records are the only broadly supported record type that allow pointing an apex domain (no subdomain or www prepending it). CNAME records do not.
  • Example: customdomain.com (A record) your cluster IP address
Using an intermediary with CNAMEs
Another option is to point an intermediary domain or subdomain that you control at your cluster with an A record, and then have custom domains point at the intermediary with a CNAME.
There are a few things to know about this method:
  • Requests to custom domains will follow the CNAME and point wherever your intermediary is pointed.
  • You can re-point all custom domains pointed this way at once by changing the intermediary domain/subdomain.
  • Public DNS spec only allows CNAME records to point a subdomain at another domain or subdomain. You can't point an apex record using a CNAME, so it will need to have a subdomain or www prepending it.
  • This will work:
    www.customdomain.com (CNAME) domains.myapp.com (A record) your cluster IP address
  • This will not work because it points an apex domain with a CNAME:
    customdomain.com (CNAME) domains.myapp.com (A record) your cluster IP address
TL;DR
If you're okay with your custom domains all having www or having another subdomain, use an intermediary domain with CNAMEs. Otherwise, use A records.

Using API Keys

Approximated requires an private API key to accept API requests from your application. This API key should never be made public or added to client-side javascript code.

You can create or get an existing API key from the dashboard, and add it to all API requests under the request header "api-key".

Virtual Hosts API

The virtual hosts API is how your application will interface directly with Approximated. You can use it to automate custom domains as needed. There are four endpoints to create, read, update, and delete a virtual host.

Note:
Make sure that you include headers for Content-Type and Accept set to 'application/json' for all requests to the API.

Create Virtual Host

Creating a virtual host is done with a JSON POST request to the Approximated API. It can be created at any time before or after the custom domain is pointed at the cluster with a DNS record.

POST
https://cloud.approximated.app/api/vhosts
Returns
201 - Successfully created
{
  "data": {
    "id": 445922,
    "incoming_address": "acustomdomain.com",
    "target_address": "myapp.com",
    "target_ports": "443",
    "user_message": "In order to connect your domain, you'll need to have a DNS A record that points acustomdomain.com at 213.188.210.168. If you already have an A record for that address, please change it to point at 213.188.210.168 and remove any other A records for that exact address. It may take a few minutes for your SSL certificate to take effect once you've pointed your DNS A record."
  }
}
        
401 - The API key used does not exist
Fields
incoming_address
Required
String

The custom domain that you'd like to route.

Example: acustomdomain.com
target_address
Required
String

The address that you'd like requests for the custom domain to be routed to. Typically another domain or domain with a sub-page.

Example: myapp.com, myapp.com/some/page
target_ports
Optional
String
Default: "443"

This sets the port that you'd like requests to arrive at on the target address. By default it is port 443 as that is the port that web traffic secured by SSL is typically served from.

Example: 443, 80, 8080 (string)
redirect
Optional
Boolean
Default: false

Set this to true if you'd like to have requests be 301 redirected to the target address instead of proxied.

Note: redirects need to have the protocol (http:// or https://) included in the target_address, or they will be appended to the incoming address.

Example: true or false (boolean)
exact_match
Optional
Boolean
Default: false

Set this to true if you'd like to have requests that exactly match the incoming address, including paths, be overridden and routed somewhere specific. Typically this is used in combination with another virtual host configured for the base custom domain.

Note: this will ignore any extra user-added paths or queries if it matches, and will override any other virtual hosts for the same domain that don't exactly match.

Example: true or false (boolean)
redirect_www
Optional
Boolean
Default: false

For convenience, when set to true, Approximated will create a second virtual host as well that will 301 redirect the www version of the incoming address to this address.

Example: true or false (boolean)
keep_host
Optional
Boolean
Default: null (uses cluster default)

Set this to true if you'd like the Host header to be left as the incoming address (the custom domain) for this virtual host. Set to false if you'd like to override the cluster default setting for this, or null if you'd like to use the default setting.

Note: when set to false, either by default or when explicitly set here, your cluster will change the Host header for each request to the target address by default. This can often avoid issues with servers/reverse proxies out of the box.

Example: true, false, null (boolean)

Read Virtual Host

Use a GET request with the incoming_address at the end of the URL to retrieve the details of a single Virtual Host. If that Virtual Host was created with the API Key included in the header, it's details will be returned.

GET
https://cloud.approximated.app/api/vhosts/by/incoming/:incoming_address
Returns
200 - Successfully returned Virtual Host
{
  "data": {
    "apx_hit": true, // requests are reaching the cluster
    "created_at": "2023-04-03T17:59:28", // UTC timezone
    "dns_pointed_at": "213.188.210.168", // DNS for the incoming_address
    "has_ssl": true,
    "id": 405455,
    "incoming_address": "acustomdomain.com",
    "is_resolving": true, // is this returning a response
    "last_monitored_humanized": "1 hour ago",
    "last_monitored_unix": 1687194590,
    "ssl_active_from": "2023-06-02T20:19:15", // UTC timezone
    "ssl_active_until": "2023-08-31T20:19:14", // UTC timezone, auto-renews
    "status": "ACTIVE_SSL",
    "status_message": "Active with SSL",
    "target_address": "myapp.com",
    "target_ports": "443"
  }
}
        
404- Could not find Virtual Host with that API key
401 - The API key used does not exist

In order to avoid spamming your custom domains with monitoring checks every time you call this endpoint, Approximated returns the results of the most recent recorded status check, which may be out of date.

If you'd prefer to have it check again before responding, you can add /force-check to the end of the endpoint URL. This may take up to 30 seconds if the domain DNS is not pointed yet, and is rate limited to minimize accidentally DDOSing your application.

Update Virtual Host

Updating a virtual host is done with a JSON POST request to the Approximated API. It can be updated at any time before or after the custom domain is pointed at the cluster with a DNS record. Any optional fields not submitted will remain the same as they were previously.

POST
https://cloud.approximated.app/api/vhosts/update/by/incoming
Returns
200 - Successfully updated
{
  "data": {
    "apx_hit": true, // requests are reaching the cluster
    "created_at": "2023-04-03T17:59:28", // UTC timezone
    "dns_pointed_at": "213.188.210.168", // DNS for the incoming_address
    "has_ssl": true,
    "id": 405455,
    "incoming_address": "adifferentcustomdomain.com",
    "is_resolving": true, // is this returning a response
    "last_monitored_humanized": "1 hour ago",
    "last_monitored_unix": 1687194590,
    "ssl_active_from": "2023-06-02T20:19:15", // UTC timezone
    "ssl_active_until": "2023-08-31T20:19:14", // UTC timezone, auto-renews
    "status": "ACTIVE_SSL",
    "status_message": "Active with SSL",
    "target_address": "myapp.com",
    "target_ports": "443"
  }
}
        
404 - Could not find an existing Virtual Host with that incoming address
401 - The API key used does not exist
Fields
current_incoming_address
Required
String

The custom domain for an existing Virtual Host.

Example: acustomdomain.com
incoming_address
Optional
String

A new custom domain you would like to change the existing Virtual Host to.

Example: adifferentcustomdomain.com
target_address
Optional
String

The address that you'd like requests for the custom domain to be routed to. Typically another domain or domain with a sub-page.

Example: myapp.com, myapp.com/some/page
target_ports
Optional
String
Default: "443"

This sets the port that you'd like requests to arrive at on the target address. By default it is port 443 as that is the port that web traffic secured by SSL is typically served from.

Example: 443, 80, 8080 (string)
redirect
Optional
Boolean
Default: false

Set this to true if you'd like to have requests be 301 redirected to the target address instead of proxied.

Note: redirects need to have the protocol (http:// or https://) included in the target_address, or they will be appended to the incoming address.

Example: true or false (boolean)
exact_match
Optional
Boolean
Default: false

Set this to true if you'd like to have requests that exactly match the incoming address, including paths, be overridden and routed somewhere specific. Typically this is used in combination with another virtual host configured for the base custom domain.

Note: this will ignore any extra user-added paths or queries if it matches, and will override any other virtual hosts for the same domain that don't exactly match.

Example: true or false (boolean)
redirect_www
Optional
Boolean
Default: false

For convenience, when set to true, Approximated will create a second virtual host as well that will 301 redirect the www version of the incoming address to this address.

Example: true or false (boolean)
keep_host
Optional
Boolean
Default: null (uses cluster default)

Set this to true if you'd like the Host header to be left as the incoming address (the custom domain) for this virtual host. Set to false if you'd like to override the cluster default setting for this, or null if you'd like to use the default setting.

Note: when set to false, either by default or when explicitly set here, your cluster will change the Host header for each request to the target address by default.

Setting this to false can often avoid issues with servers/reverse proxies out of the box, but you'll need to use the apx-incoming-host header in your app to determine the custom domain.

Example: true, false, null (boolean)

Delete Virtual Host

Use a DELETE request with the incoming_address at the end of the URL to remove a single Virtual Host.

DELETE
https://cloud.approximated.app/api/vhosts/by/incoming/:incoming_address
Returns
200 - Successfully deleted Virtual Host
Deleting customdomain.com
404- Could not find Virtual Host with that API key
401 - The API key used does not exist

DNS Checks API

The DNS checks API allows you to check if DNS records exist for a domain or subdomain. It's provided as a convenience for your application, for scenarios like:

  • Ensuring a user has pointed an A or CNAME record.
  • Using TXT records to validate domain ownership.
  • Helping you to support/debug DNS issues for your users.

Note:
Make sure that you include headers for Content-Type and Accept set to 'application/json' for all requests to the API, as well as including an Api-Key header with a current API key.

Check DNS Records Match Exactly

Send a JSON POST request to the Approximated API with a list of data to compare against DNS records. Determines if an address has exactly one matching record for each data object in the list.

This check returns the list back to you with the results for 'match' and 'actual_values' injected into each object.

The 'match' field will only be true if there is exactly one DNS record/value for each address, and it must exactly match the 'match_against' value you've set.

POST
https://cloud.approximated.app/api/dns/check-records-match-exactly
Returns
200 - Check completed
{
  "records": [
    {
        "actual_values": [
            "93.184.216.34"
        ],
        "address": "example.com",
        "match": false,
        "match_against": "12.345.678.90",
        "type": "a"
    }
  ]
}
401 - The API key used does not exist
Fields
records
Required
Array

A list of DNS records you'd like to check.

Example:
{
  "records": 
    [
      {
        "address": "example.com",
        "match_against": "12.345.678.90",
        "type": "a"
      }
    ]
}
record[address]
Required
String

The address of the record you want to check. Typically a domain/subdomain. Do not use placeholders like @ that you may see in DNS dashboards, but rather the complete address.

Example: myapp.com, subdomain.myapp.com
record[type]
Required
String

The DNS record type you'd like to check, formatted as a lowercase string.

Example: "a", "cname", "ns", "txt"
record[match_against]
Required
String

This is the value that you'd like the record value to be compared against. The value here can be any string, as all record values will be converted to strings.

Note: there can be multiple records/values for the same address. This endpoint will only return "match": true if there is exactly one record, and that record exactly matches this field.

Example: "12.345.678.90", "5.alt1.aspmx.l.google.com"

Check DNS Records Exist

Send a JSON POST request to the Approximated API with a list of data you'd like to check against the DNS records of an address.

DNS allows multiple records for the same address, and you may wish to check that at least one of them matches while disregarding the rest. This check is for that purpose.

The data list is returned back to you with injected fields for 'match' and 'actual_values'. The 'match' field will be true if there is any record/value that matches the 'match_against' field, regardless of how many other records/values there may be for that address.

POST
https://cloud.approximated.app/api/dns/check-records-exist
Returns
200 - Check completed
{
  "records": [
    {
        "actual_values": [
            "93.184.216.34"
        ],
        "address": "example.com",
        "match": false,
        "match_against": "12.345.678.90",
        "type": "a"
    }
  ]
}
401 - The API key used does not exist
Fields
records
Required
Array

A list of DNS records you'd like to check.

Example:
{
  "records": 
    [
      {
        "address": "example.com",
        "match_against": "12.345.678.90",
        "type": "a"
      }
    ]
}
record[address]
Required
String

The address of the record you want to check. Typically a domain/subdomain. Do not use placeholders like @ that you may see in DNS dashboards, but rather the complete address.

Example: myapp.com, subdomain.myapp.com
record[type]
Required
String

The DNS record type you'd like to check, formatted as a lowercase string.

Example: "a", "cname", "ns", "txt"
record[match_against]
Required
String

This is the value that you'd like the record value to be compared against. The value here can be any string, as all record values will be converted to strings.

Note: there can be multiple records/values for the same address. This endpoint will return "match": true if any one of them matches.

Example: "12.345.678.90", "5.alt1.aspmx.l.google.com"

Integrating With Your App

When a request for a custom domain reaches your application, you likely want to return content specific to that custom domain. For example, if your app hosts blogs, the custom domain should return content for that particular blog.

The sections below should help guide you in making any changes necessary to your application in order to handle custom domains.

The Request/Response Flow

When a request goes through your Approximated cluster, it's relayed as shown below:

Approximated provides SSL encryption between the user and the cluster, but only your app or server can provide SSL encryption between the cluster and your app.

If your target address is a naked IP address, you won't be able to SSL encrypt a connection to it or use port 443 (typically reserved for SSL connections).

Luckily, most applications already have a domain or subdomain pointed at their application with an SSL certificate. It depends on your app, but it's very likely you can simply re-use this as your target address for custom domains.

Determining the custom domain

Approximated has a few ways that your app can determine the custom domain for a request.

  1. (Default) Apx-Incoming-Host header

    By default, Approximated will change the host header in each request to match the target address instead of the incoming address (the custom domain).

    We've made this choice because often apps, servers, or reverse proxies are not ready to handle any domain but the primary app domain without modifications.

    We always inject an extra header "Apx-Incoming-Host" to every request, which the app can use to determine the custom domain when it receives the request.

  2. Keep the Host header as-is

    Alternatively, you can set your proxy cluster to keep the Host header for each request as the incoming address instead of modifying it.

    You can find this by opening your proxy cluster in the dashboard and changing the Keep Host Headers setting to True. This can be overridden for each virtual host, as well.

    To do so, in the dashboard under advanced settings you can set Keep Host to True/False/Default. In the API, you can set keep_host to true/false/null to achieve the same.

  3. Send the X-Forwarded-Host header

    Finally, you can set your proxy cluster to add an X-Forwarded-Host header containing the incoming address for each request. This is independent of the other settings.

    You can find this by opening your proxy cluster in the dashboard and changing the Send X-Forwarded-Host setting to True.

Statically Generated Content

With statically generated content, you probably have folders and files sitting on a server for each custom domain. For example, a request to mybloghost.com/some-blog will load the content directly from the /some-blog folder on your server.

In this situation, there's probably no code being run before that content is loaded directly from the files. You might have caching, but the end result is the same.

For statically generated sites, the easiest way to integrate custom domains is likely to target that user's folder URL directly. With Approximated, you can accomplish this by adding a path to your virtual host target address field.

For example:

Incoming Address:
someblog.com

Target Address:
https://mybloghost.com/some-blog

* Note the https:// in the target address is required when using a path

There are some obstacles to this approach, however:

  1. Additional appended paths may not exist

    All paths that are appended to to the custom domain will also be appended to the end of the target, which may cause issues in some cases.

    Example #1
    incoming_address: someblog.com
    target_address: https://mybloghost.com/some-blog
    Going to this URL:
    someblog.com/some-blog/some-post
    Will reach this URL on your server:
    https://mybloghost.com/some-blog/some-post
    This is probably what you want, so there's likely no issue here.
    Example #2
    Incoming Address: someblog.com
    Target Address: https://mybloghost.com/some-blog
    Going to this URL:
    someblog.com/assets/app.css
    Will reach this URL on your server:
    https://mybloghost.com/some-blog/assets/app.css

    If your application is using shared assets like app.css or app.js for all custom domains, then your code is probably expecting to find them at:

    mybloghost.com/assets/app.css
    Instead of at:
    mybloghost.com/some-blog/assets.app.css

    In that case, this URL will 404 on the custom domain because app.css is not located there.

    Solutions:
    • Symlink folders like assets in each statically generated folder. Typically your server will return requests to anything within as if they were actually there.
    • Set URLs for things like assets to be absolute in the generated HTML code.

      For example:

      href="https://mybloghost.com/assets/app.css"

      Instead of:

      href="/assets/app.css"

      Note: you may run into CORS policy issues with this approach. See below for more information.

  2. Assets on your primary domain may be restricted by CORS for custom domains

    If you make a request to your main app, for instance to get assets, you may get a CORS error if you have a CORS policy restricting other domains.

    Example:
    Custom domain: someblog.com
    Your app domain: mybloghost.com
    Linking an asset like this:
    <link rel="stylesheet" href="https://mybloghost.com/assets/app.css" />
    From a custom domain may result in a CORS error if you have a CORS policy restricting other domains from loading your app's content.
    Solutions:
    • Allow all origins in your CORS policy for those URLs by setting it to the wildcard "*".

      Note: this could have security implications for your application, please consider how loading this content on other domains might impact you first.

    • Provide those assets relative to your custom domain as well, either by generating them in each user folder or symlinking.

    • Use a CDN for assets that will allow your custom domains with CORS.

Laravel

Laravel is one of the easiest frameworks to integrate with Approximated. We've created an entire guide for you, and a Laravel custom domains example repo to make things even easier.

You won't need to install any additional packages (security team high five!) and the guide covers every aspect you'll need to know about custom domains in a Laravel app - from request to response.

Ruby on Rails

Ruby on Rails makes integrating with Approximated easy. The best way to learn how to integrate Approximated with your Rails app is to look at our Ruby on Rails custom domains guide and it's complimentary github example repo.

That guide and repo show a working example that demonstrates creating pages that can be tied to a custom domain, the routing required, and a class that interfaces with the Approximated API for you.

Elixir Phoenix

Apps using Elixir's Phoenix framework can integrate with Approximated easily, including websockets for liveview. We've created a comprehensive developers guide for supporting custom domains in Phoenix to help you get up and running as fast as possible.

We've also created a companion example repo here for you to explore and run easily.

The example repo is a simple blog hosting platform, where you can create blogs and tie them to a custom domain. It should serve as a reference for how your Phoenix app can handle routing, liveviews/websockets, security features, and more for custom domains.