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:
.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 emoji gets converted by the browser to a punny code (
â xnâ158h) then the Worker intercepts the requests and performs the following actions:
- The requested subdomain â
â is captured by the Worker (the browser automatically translates the Emoji to its punny code
â xnâ158h)
- 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â
- The Worker determines if this is a âversion-testâ request or not
- The Worker builds the new URL âWUN |
- 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:
- Exposed a Workflow Endpoint to validate the HMAC and return an OAuth redirect URL
- Created a proxied fake DNS record where we can send âApp Installâ requests to
- 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
- 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.
- 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.
- 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.
- 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 . Comments? Feedback? Please let us know by leaving a reply
Ali Farahat