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
// Example response:
{
  "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."
  }
}
422 - Validation errors
// Example response
{
  "errors": {
    "incoming_address": [
      "This incoming address has already been created on the cluster you selected."
    ]
  }
}
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)

List Virtual Hosts

We use a cursor based pagination system to page through your virtual hosts, up to 20 at a time. You can get the first page of the list by calling the first endpoint below, without a cursor.

You'll receive back a JSON object with a data field that contains the list of virtual hosts, as well as an after_cursor and before_cursor. You can then get the next or previous page by calling the endpoints below with the after or before cursor from the current results.

GET (first page)
https://cloud.approximated.app/api/cursor/vhosts
GET (next page)
https://cloud.approximated.app/api/cursor/vhosts/after/:after_cursor
GET (previous page)
https://cloud.approximated.app/api/cursor/vhosts/before/:before_cursor
Returns
200 - Successfully returned list of Virtual Host
// Example response
{
  "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"
    },
    ...
  ],
  after_cursor: "a39fdk32kf", // will be null if there is no next page
  before_cursor: "lf3jeuc3406" // will be null if there is no previous page
}
        
401 - The API key used does not exist

The monitoring fields and statuses are from the latest monitoring results. A fresh check is not performed for the list before responding, and it cannot be force checked like an individual virtual host.

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.

Note: to get by incoming incoming address, it must not include paths, query strings, or a protocol like https:// in the incoming address value. If you've included those in the virtual host you'd like to get, please use the alternative POST endpoint below that takes a JSON object with incoming_address instead.

GET
https://cloud.approximated.app/api/vhosts/by/incoming/:incoming_address
POST (alternative)
https://cloud.approximated.app/api/vhosts/by/incoming
{"incoming_address": "acustomdomain.com"}
Returns
200 - Successfully returned Virtual Host
// Example response
{
  "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
// Example response
{
  "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"
  }
}
        
422 - Validation errors
// Example response
{
  "errors": {
    "incoming_address": [
      "This incoming address has already been created on the cluster you selected."
    ]
  }
}
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 or with the virtual host ID, to remove a single Virtual Host.

Note: to delete by incoming incoming address, it must not include paths, query strings, or a protocol like https:// in the incoming address value. If you've included those in the virtual host you'd like to delete, please use the alternative POST version of this endpoint that takes a JSON request object with a field for incoming_address instead.

DELETE
https://cloud.approximated.app/api/vhosts/by/incoming/:incoming_address
POST (alternative)
https://cloud.approximated.app/api/vhosts/delete/by/incoming
{"incoming_address": "acustomdomain.com"}
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"

DNS Widget

We've created an optional DNS widget that's embeddable into your app, to help your users update their DNS records when connecting a custom domain.

The DNS widget will detect the DNS provider for custom domains and offer one-click automatic DNS updates to the user when possible. In cases where the DNS provider does not allow automation, the widget will generate instructions specific to the DNS provider and the custom domain so that you don't have to.

DNS Widget Demo

You can see a working example of the widget with default styling by clicking the button below:

DNS Widget Basics

  • The DNS widget is generated using a simple, plain javascript file that we provide.
  • It's styled with a simple, customisable CSS file that we provide.
  • It does not require an iframe so that you have full control over everything.
  • The widget is started by calling it's init function in javascript.
  • The config object contains some settings and an array of the DNS records you'd like to apply to custom domains.
  • You can optionally add a domain to the config to skip the domain entry UI step when the custom domain is already known.
  • The widget calls Approximated API endpoints to detect domain DNS records, generate instructions/automation steps, and to verify changes.
  • The instructions and automation steps are rendered on-demand in Approximated and returned to the widget as HTML.
    We may optionally provide them as pure JSON objects in the future, so if you'd benefit from that then please let us know!

DNS Widget Setup

We've tried to make the DNS widget easily compatible with as many different stacks and approaches as possible. You can follow the instructions below to get started quickly.

  1. Link the javascript file from Approximated:
    <link src="https://cloud.approximated.app/dnswidget/dnswidget.v1.js">
  2. Link the CSS file from Approximated:
    <link rel="stylesheet" href="https://cloud.approximated.app/dnswidget/dnswidget.v1.css">
    Note: you can take a look at this file and override styles and variables as needed with your own CSS as well.
  3. Call the widget init function using javascript:
    // Create the config data for the widget.
    const apx_dns_widget_config = {
        // A 10 minute token is generated server-side to allow access to the API.
        token: "use_token_api_endpoint_server_side_to_generate_this", 
        api_url: "https://cloud.approximated.app/api/dns",
        /* 
        * The dnsRecords field is an array of record objects 
        * that you'd like to have applied to custom domains. 
        * 
        * These will likely be A records pointing at your cluster IP address,
        * or CNAME records pointing at an intermediary domain you control.
        * The dnsRecords always consist of:
        *      - [string]  type (Uppercase DNS record type like A, CNAME, or TXT)
        *      - [string]  host (Use @ for the apex domain or a subdomain with no trailing dot)
        *      - [string]  value (the address, value, text, etc. for that record type)
        *      - [integer] ttl (how many seconds the record should be cached in DNS servers)
        * We currently allow setting A, CNAME, and TXT records with the DNS widget.
        */
        dnsRecords: [
            {
                type: "A",
                host: "@",
                value: "123.456.789.01",
                ttl: 3600,
            },
            {
                type: "CNAME",
                host: "@",
                value: "example.com",
                ttl: 3600,
            }
        ],
        domain: "customdomain.com", // optional, skips the domain entry UI step
        prefillDomain: "customdomain.com", // optional, prefills the domain without skipping
        verifyAutoScroll: true // optional, auto-scroll to verification results. Default: true
    };
        
    // Call the DNS widget init function to launch the widget.
    // In this example we call it after the DOM content is loaded.
    document.addEventListener("DOMContentLoaded", function(event) {
        // The DNS widget functions/data are automatically namespaced
        // and available from window.apxDns.
        window.apxDns.init(apx_dns_widget_config);
    });
    
  4. Optionally, set javascript listeners to call some of your own logic based on widget events.
    // When the user submits a domain in the UI
    document.addEventListener('apx-dnswidget-user-submitted-domain', function(event) {
        // Your code to handle the event.
        // The event.detail.domain will contain the custom domain/subdomain 
        // submitted by the user into the UI.
    });
    
    // When the widget flow is restarted
    document.addEventListener('apx-dnswidget-restarted', function(event) {
        // Your code to handle the event.
    });
    
    // When all of the records are completely verified as updated successfully
    document.addEventListener('apx-dnswidget-records-completely-verified', function(event) {
        // Your code to handle the event.
        // The event.detail will contain the record results as a list of objects
    });
    
    // When none of the records match the desired updates, during verification
    document.addEventListener('apx-dnswidget-records-failed-verification', function(event) {
        // Your code to handle the event.
        // The event.detail will contain the record results as a list of objects
    });
    
    // When only some of the records are verified as updated successfully
    document.addEventListener('apx-dnswidget-records-partially-verified', function(event) {
        // Your code to handle the event.
        // The event.detail will contain the record results as a list of objects
        console.log(event.detail);
    
        // the output of the console.log above could look like this:
        [
            {
                actual_values: [
                    "123.456.789.01"
                ],
                address: "example.com",
                apex: "example",
                domain: "example.com",
                full: "example.com",
                host: "@",
                match: true, // Approximated found an exact match DNS record
                match_against: "123.456.789.01", // The value to match against the DNS record
                non_tld: "example",
                subdomain: "example",
                tld: "com",
                type: "a",
                value: "123.456.789.01"
            },
            {
                actual_values: false,
                address: "example.com",
                apex: "example",
                domain: "example.com",
                full: "example.com",
                host: "@",
                match: false, // Approximated could not find a matching DNS record
                match_against: "anotherexample.com", // The value to match against the DNS record
                non_tld: "example",
                subdomain: "example",
                tld: "com",
                type: "cname",
                value: "anotherexample.com"
            }
        ]
    });
    
  5. You can optionally stop and clear the widget element or restart the flow.
    // Stop and clear the element used for the widget.
    // The init function will need to be called again to restart.
    window.apxDns.stop()
    
    // Alternatively, you can restart without stopping by calling this function:
    window.apxDns.restart()
    

DNS Widget Styling

The DNS widget has some fairly neutral styling applied already, but you can override it with your own CSS as needed. It's outermost element has a default width of 700px applied, but has no height, padding, or borders applied by default so that you can more easily place it within your app.

Every CSS class in the element is prefixed with apxdns-, and the outermost element always has a class of apxdnswidget. The intention for this is to avoid any class name conflicts with your existing CSS code.

The widget also makes use of CSS variables scoped to the class apxdnswidget to enable easier theming. The available variables and their default values are as follows:

.apxdnswidget {
    --text-color: #333;
    --light-text-color: #666;
    --link-text-color: rgb(46, 90, 173);
    --main-bg-color: #FFF;
    --shaded-bg-color: #FAFAFA;
    --shaded-border: 1px solid #EEE;
    --button-bg-color: #333;
    --button-text-color: #FFF;
    --radius: 5px;
    --widget-max-width: 700px;
    --border-color: #EEE;
}

DNS Widget Token

To prevent abuse, we require a client-side public token to be generated with a server-side call to our API DNS Token endpoint. This will return a token that can be added to the widget config data.

Note: make sure you include the API key for your proxy cluster whenever using the API.

GET
https://cloud.approximated.app/api/dns/token
Returns
200 - Success
{
    "token": "a-valid-token"
}
401 - The API key used does not exist
No fields/data required.

The token expires in 10 minutes, but if the page is still open, the widget will automatically use the existing token to get a new, valid token before 10 minutes are up.

DNS Widget Versioning

To provide improvements and new features without unexpected surprises, we version the DNS widget with simple integer versioning in the form of dnswidget.v1.js and dnswidget.v1.css.

Patches, fixes, and other non-breaking updates will update the existing version, while any breaking changes will be placed on a newer version. You can remain on older versions as long as needed, and should assume any new version will need to be tested with your application before deploying.

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.

Next.js

Next.js can be used with Approximated, whether you host it with Vercel or anywhere else. We've created a comprehensive developers guide for supporting custom domains in Next.js to help you get up and running as fast as possible, which includes examples using both the App and Page routers.

We've also created a companion example repo here for you to explore and run easily, to get a sense of how Approximated could be used to integrate custom domains into your Next.js app.