Subdomain redirect - possible with Bubble?

We wanted to take some time out of our busy schedules to share a tip that can help the community, as we do get this question a lot! The short answer is yes, you can create a subdomain redirect to work with your bubble app; and yes they can be built to be smart!

So what do we mean by a subdomain redirect? Well, imagine you have a user that accesses a specific page on your app by navigating to a URL like this:

example.com/portfolio/the-upstarters-agency

As you can see, the URL is long and hard to remember. But what if we can provide the user with a new URL (that is a subdomain) that is shorter and automatically forwards them to the right page? Maybe something like this:

upstarters.example.com → Which then forwards to the above long URL

It can even be something that utilizes an emoji subdomain:

:rocket:.wun.vc → wun.vc/id/alifarahat

How can you replicate something like this on your own app? To make the redirects possible we need to run a JS (JavaScript) Worker when a DNS request is made. Naturally, we went with Cloudflare for our DNS management and used their Workers to perform this task.

Cloudflare Workers are written in JavaScript and they can be attached to an HTTP Route. The Route triggers the Worker and executes the code. In the second example above, the :rocket: emoji gets converted by the browser to a punny code (:rocket: → xn–158h) then the Worker intercepts the requests and performs the following actions:

  1. The requested subdomain “:rocket:” is captured by the Worker (the browser automatically translates the Emoji to its punny code :rocket: → xn–158h)
  2. An API call is made to Bubble to lookup the subdomain and returns the ID of the database record. In this case it’s “alifarahat”
  3. The Worker determines if this is a ‘version-test’ request or not
  4. The Worker builds the new URL “WUN |
  5. The Worker redirects the browser request using a 301 or a 302

Cloudflare Workers are a great tool to solve more complex problems as well. We recently had a project that required an OAuth integration with Shopify, and for us to publish the app in the Shopify Store we needed to handle unique requirements that Bubble could not solve natively.

When the install request occurs from the Shopify Store for the Bubble app, they provided us with a HMAC (a type of message authentication code involving a cryptographic hash function and a secret cryptographic key) that needed to be validated before the consent screen can be displayed to the user. If the HMAC is authenticated then we can proceed with the OAuth process, otherwise we should return an error. The caveat here is when the user clicks “Install App” in Shopify, then it should immediately be forwarded to the OAuth consent screen in Shopify without landing/stopping at any other page. These are the steps we took to make this happen:

  1. Exposed a Workflow Endpoint to validate the HMAC and return an OAuth redirect URL
  2. Created a proxied fake DNS record where we can send “App Install” requests to
  3. Created a Worker that took in the Install Request URL → called the API Endpoint in Bubble.io → forwarded the request using a 301 redirect to the Shopify page

So how to set this up yourself? Well here is a simplified guide

  1. Migrate your DNS to Cloudflare. It’s free and the process is painless and Cloudflare does a great job in importing your current DNS configuration. Be sure to toggle ‘Proxied’ to ‘off’ so Bubble.io does not generate an error.

  1. Install the “Cloudflare DNS Management” Bubble plugin. You will need this to dynamically add DNS records to your Cloudflare site. If you are on an enterprise plan then you may not need this as you can utilize a Wildcard DNS Record to handle all requests made to the Worker. But the majority of us will need to create the DNS record for every subdomain request made.

  1. Call the “Create DNS Record” action in the plugin to create your first one. You should leave the content to this IP “192.0.2.1”. Also leave the proxied field to “true”. Since the IP is a dummy and it is proxied when the reque st will be sent to the Worker for processing. You will need an API key to make the request, so make sure you created one for the zone (site) you want to modify.

The next step is to create your Worker. To do this, head over to the Cloudflare Worker tab. I will leave a simple example below.

  1. The last step is to define your route. The route is important because it’s responsible for routing the request to the Worker. The Worker will not become active until it is part of a route. In our case it looks like this:

Sample Cloudflare Worker Code

const destinationProtocol = "https://"
const destinationBase = "example"
const destinationTL = "com"
const destinationURLDev = `${destinationProtocol}${destinationBase}.${destinationTL}/version-test/portfolio/`
const destinationURLProd = `${destinationProtocol}${destinationBase}.${destinationTL}/portfolio/`

//Reserved Subdomains
const reservedSubdomains = ['www', 'blog', 'shop', 'help', 'support', "app"]

const statusCode = 301

function GenDestURL(version, gotoShop, requestedEmoji) {
let destinationFinalURL
const destinationProtocol = "https://"
const destinationBase = "example"
const destinationTL = "com"
const destinationBaseURL = `${destinationProtocol}${destinationBase}.${destinationTL}`

if (gotoShop) {
let ver = (version) ? `/${version}/` : '/'
return `${destinationBaseURL}${ver}?requested_portfolio=${requestedEmoji}`
}

if (version) {
destinationFinalURL = `${destinationBaseURL}/${version}/portfolio/`
return destinationFinalURL
} else {
destinationFinalURL = `${destinationBaseURL}/portfolio/`
return destinationFinalURL
}
}

const apiInit = {
method: 'get',
headers: {
"Authorization": "Bearer 27****"
},
}

let punycodeLookup

async function extractID(response) {
let r
const {
status
} = response;
let res = await response.text().then((d) => {
r = d
})
return r
}

async function handleRequest(request) {
const url = new URL(request.url)
const {
pathname,
search,
protocol,
href
} = url

let reqSubdomain = href.split('.')[1] ? href.split('.')[0] : false;
let dotLength = href.match(/\./g).length;
reqSubdomain = reqSubdomain.replace(protocol + '//', '');

//check if Slug is active in GR
const init = {
headers: {
"content-type": "application/json;charset=UTF-8",
"Authorization": "Bearer ****"
},
}

console.log({
'reqSubdomain': reqSubdomain,
'originalRequest': href,
"noDots": href.match(/\./g).length
})
//Check if subdomain is provided and not reserved
if (!reqSubdomain href.match(/\./g).length <= 1 reservedSubdomains.includes(reqSubdomain)) {
return fetch(request)
}

//Set the api urls
const apiURLDev = `https://example.com/version-test/api/1.1/obj/***?constraints=[{"key":"Slug","constraint_type":"equals","value":"${reqSubdomain}"},{"key":"active","constraint_type":"equals","value":"true"}]`
const apiURLProd = `https://app.example.com/api/1.1/obj/***?constraints=[{"key":"Slug","constraint_type":"equals","value":"${reqSubdomain}"},{"key":"active","constraint_type":"equals","value":"true"}]`

//Handle redirect
//Determine Bubble.io Version
let bubbleVersion = pathname.replace(/^https?:\/\//, '').split('/');
let apiURL = bubbleVersion[1].includes('version-') == true ? apiURLDev : apiURLProd
console.log(apiURL)

//Get data from Bubble API
const response = await fetch(encodeURI(apiURL), apiInit)
const data = JSON.parse(await extractID(response))
const targetCount = ('response' in data) && ('count' in data.response) ? data.response.count : 0
const targetData = data.response.results[0]
console.log(data.response)

//If Available
if (targetCount > 0) {
if (('Slug' in targetData)) {
let destinationURL = GenDestURL(bubbleVersion[1], false, '')
let path = targetData.Slug
return Response.redirect(destinationURL + path, statusCode)
} else {
let destinationURL = GenDestURL(bubbleVersion[1], true, reqSubdomain)
console.log('Not attached portfolio > send to index', destinationURL)
return Response.redirect(destinationURL, statusCode)
}
} else {
//No results
let destinationURL = GenDestURL(bubbleVersion[1], true, reqSubdomain)
return Response.redirect(destinationURL, statusCode)
}

}

addEventListener("fetch", async event => {
event.respondWith(handleRequest(event.request))
})

We love Bubble, and we are always experimenting and trying to extend what it can do :muscle:. Comments? Feedback? Please let us know by leaving a reply

Ali Farahat


Theupstarters.com

9 Likes

@AliFarahat is there a way making this solution but without the redirect? Having multiple subdomains on a single Bubble application.

For example:
-Having example.com as a landing page and
-Multiple subdomains such as product1.example.com, product2.example.com
All running in the same bubble app.

If you have any suggestions regarding this issue it would be great!
Thanks in advance!

Hi @AliFarahat

Thanks for sharing this. A really interesting approach to handling the unique OAuth flow that Shopify requires.

Just out of interest, would you know of any other way of setting up the Shopify OAuth flow without having to set up the subdomain redirect? I’m a bit hesitant to try your method due to my limited technical knowledge.

Many thanks,
Adam

No, you will need the redirect. So basically product1.example.com > some URL.

You can probably employ a URL rewrite, although I am not sure what the consequences of that will be

Hey Adam,

we have tried many different ways. Unfortunately Bubble does not have any tools that help us handle this another way for Shopify OAuth

1 Like