[{"data":1,"prerenderedAt":1998},["ShallowReactive",2],{"\u002Fblog\u002Fmulti-tenancy-with-nuxt-and-cloudflare":3,"\u002Fblog\u002Fmulti-tenancy-with-nuxt-and-cloudflare-surround":1853,"ungh-user-egoist":1854,"ungh-user-zernonia":1860,"ungh-user-atinux":1864,"$fKDBs0Mt1-RPu9S1h1ahHLSF3KOo2X04KzIbaiN3Xml0":1869},{"id":4,"title":5,"body":6,"date":1813,"description":1814,"extension":1815,"links":1816,"meta":1817,"navigation":267,"ogImage":1816,"other":1816,"path":1818,"readingTime":495,"references":1819,"robots":1816,"schemaOrg":1816,"seo":1846,"sitemap":1847,"stem":1848,"tags":1849,"__hash__":1852},"blog\u002Fblog\u002Fmulti-tenancy-with-nuxt-and-cloudflare.md","Multi-tenancy with Nuxt and Cloudflare",{"type":7,"value":8,"toc":1796},"minimark",[9,16,21,24,71,78,82,92,97,151,186,189,651,676,731,735,741,804,807,1235,1244,1247,1397,1401,1408,1467,1473,1477,1486,1489,1500,1514,1518,1538,1542,1587,1601,1605,1608,1659,1663,1670,1738,1756,1760,1768,1772,1787,1792],[10,11,12],"p",{},[13,14,15],"em",{},"NOTE: for now, any mention of the actual domain and project name is redacted. I'll update this post once I've finished the migration.",[17,18,20],"h2",{"id":19},"introduction","Introduction",[10,22,23],{},"Last year, I was asked to create a ticket sale platform — à la ticketmaster — for a local event. The solution they were using no longer met the requirements and as such were looking for alternatives. I agreed to do it for dirt cheap, with the idea to sell the service to possible future customers as well. They agreed. So with that in mind, I architected the application in such a way to support multiple clients and events — aka multi-tenancy.",[25,26,30],"tree",{":defaultExpanded":27,":items":28,"title":29},"[\"app\",\"pages\",\"org\"]","[{\"title\":\"app\",\"icon\":\"i-lucide-folder\",\"children\":[{\"title\":\"pages\",\"children\":[{\"title\":\"org\",\"icon\":\"i-lucide-folder\",\"children\":[{\"title\":\"[id].vue\",\"icon\":\"i-vscode-icons-file-type-vue\"}]},{\"title\":\"shop\",\"icon\":\"i-lucide-folder\",\"children\":[{\"title\":\"[id].vue\",\"icon\":\"i-vscode-icons-file-type-vue\"}]},{\"title\":\"user\",\"icon\":\"i-lucide-folder\",\"children\":[{\"title\":\"[id].vue\",\"icon\":\"i-vscode-icons-file-type-vue\"}]},{\"title\":\"index.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"}]},{\"title\":\"app.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"}]},{\"title\":\"nuxt.config.ts\",\"icon\":\"i-vscode-icons-file-type-nuxt\"},{\"title\":\"package.json\",\"icon\":\"i-vscode-icons-file-type-json\"}]","Nuxt project structure",[31,32,34,37,60],"template",{"v-slot:left":33},"",[10,35,36],{},"The straight forward implementation might look something like this:",[38,39,40,48,54],"ul",{},[41,42,43,44],"li",{},"Every user can access their account at ",[45,46,47],"code",{},"my-company.com\u002Fuser\u002F{user_id}",[41,49,50,51],{},"Organization dashboards can be found at ",[45,52,53],{},"my-company.com\u002Forg\u002F{org_id}",[41,55,56,57],{},"Ticket shops can be found at ",[45,58,59],{},"my-company.com\u002Fshop\u002F{shop_id}",[10,61,62,63,66,67,70],{},"This is very easy to implement in Nuxt — just create a page with brackets and any value at that location in the URL will route to that page. In the directory structure, you can see each of the dynamic pages are called ",[45,64,65],{},"[id].vue",". Based on both the value of ",[45,68,69],{},"id"," in the URL and the user's login status, you can fetch the right data.",[10,72,73,74,77],{},"This is exactly how I implemented it. The client created an organization, added ~5 users to it and then created a ticket shop. The ticket sale went flawlessly and the client was happy. So happy in fact that they told me they would like to use the service for other events as well. There was one small pain point though: the URLs. I was using my own domain which was a ",[45,75,76],{},".io"," domain. They never heard of it and were not entirely sure if their customers would trust it. The same happened when I registered my company — under which I am selling the service. The person helping me at the chamber of commerce questioned why I wouldn't just use my country's TLD. Both of them had a point. Nothing about the domain lets customers know what event they were buying tickets for. It was also impossible to just type in the search bar of their browser. Which is very important for word-of-mouth marketing.",[17,79,81],{"id":80},"implementing-subdomains","Implementing subdomains",[10,83,84,85,88,89,91],{},"I've read about subdomain based routing before and thought it would be a good solution for this problem. If you compare a domain like ",[45,86,87],{},"{some_event}.my-company.com"," to the current implementation ",[45,90,59],{},", there is immediate feedback that the site you are visiting is the one you want to be on. Instead of a random string of characters in the URL, you now have a recognizable name.",[93,94,96],"h3",{"id":95},"subdomains-in-nuxt","Subdomains in Nuxt",[10,98,99,100,107,108,112,113,116,117,122,123,128,129,132,133,136,137,142,143,146,147,150],{},"The ",[101,102,106],"a",{"href":103,"rel":104},"https:\u002F\u002Fegoist.dev\u002Fmulti-tenant-web-app-2024",[105],"nofollow","2024 multi-tenancy blog post"," by ",[109,110],"user",{"username":111},"egoist"," and ",[109,114],{"username":115},"zernonia","'s ",[101,118,121],{"href":119,"rel":120},"https:\u002F\u002Fzernonia.keypress.blog\u002Fwhy-keypress",[105],"Why Keypress blog post"," with the ",[101,124,127],{"href":125,"rel":126},"https:\u002F\u002Fgithub.com\u002Fzernonia\u002Fkeypress",[105],"Keypress github repo"," helped me understand how to implement the routing. Nuxt has a special file — ",[45,130,131],{},"router.options.ts"," — located in the ",[45,134,135],{},"app"," folder, where you can configure the ",[101,138,141],{"href":139,"rel":140},"https:\u002F\u002Frouter.vuejs.org\u002Fapi\u002Finterfaces\u002FRouterOptions.html",[105],"vue-router options",". You can add a simple function to filter the routes based on the domain. In my case, I want to map any subdomain to a specific folder in the ",[45,144,145],{},"pages"," directory — let's say ",[45,148,149],{},"pages\u002Ftenant",".",[38,152,153,165,177],{},[41,154,155,158,159,162,163],{},[45,156,157],{},"my-company.com"," should access all pages ",[13,160,161],{},"except"," the ones in ",[45,164,149],{},[41,166,167,170,171,174,175],{},[45,168,169],{},"client-1.my-company.com"," should ",[13,172,173],{},"only"," access pages in ",[45,176,149],{},[41,178,179,170,182,174,184],{},[45,180,181],{},"client-2.my-company.com",[13,183,173],{},[45,185,149],{},[10,187,188],{},"Adding the following function to the router options will filter the routes based on the subdomain.",[190,191,196],"pre",{"className":192,"code":193,"filename":194,"language":195,"meta":33,"style":33},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import type { RouterOptions } from '@nuxt\u002Fschema';\nimport type { RouteRecordRaw } from 'vue-router';\n\nconst DOMAIN = 'my-company.com';\nconst routeRegex = new RegExp(`^\\\u002Ftenant\\\u002F?`);\nconst isTenantRoute = (route: RouteRecordRaw) => route.path.match(routeRegex);\n\nexport default \u003CRouterOptions>{\n  routes: (routes) => {\n    const { hostname } = useRequestURL();\n    const subdomain = hostname.replace(`.${DOMAIN}`, '');\n\n    return routes\n      .filter((route) => (subdomain ? isTenantRoute(route) : !isTenantRoute(route)))\n      .map((route) => ({\n        ...route,\n        path: route.path.replace(routeRegex, '\u002F'),\n      }));\n  },\n};\n","app\u002Frouter.options.ts","ts",[45,197,198,237,262,269,290,334,380,385,406,426,451,493,498,507,559,581,592,628,639,645],{"__ignoreMap":33},[199,200,203,207,210,214,218,221,224,227,231,234],"span",{"class":201,"line":202},"line",1,[199,204,206],{"class":205},"s7zQu","import",[199,208,209],{"class":205}," type",[199,211,213],{"class":212},"sMK4o"," {",[199,215,217],{"class":216},"sTEyZ"," RouterOptions",[199,219,220],{"class":212}," }",[199,222,223],{"class":205}," from",[199,225,226],{"class":212}," '",[199,228,230],{"class":229},"sfazB","@nuxt\u002Fschema",[199,232,233],{"class":212},"'",[199,235,236],{"class":212},";\n",[199,238,240,242,244,246,249,251,253,255,258,260],{"class":201,"line":239},2,[199,241,206],{"class":205},[199,243,209],{"class":205},[199,245,213],{"class":212},[199,247,248],{"class":216}," RouteRecordRaw",[199,250,220],{"class":212},[199,252,223],{"class":205},[199,254,226],{"class":212},[199,256,257],{"class":229},"vue-router",[199,259,233],{"class":212},[199,261,236],{"class":212},[199,263,265],{"class":201,"line":264},3,[199,266,268],{"emptyLinePlaceholder":267},true,"\n",[199,270,272,276,279,282,284,286,288],{"class":201,"line":271},4,[199,273,275],{"class":274},"spNyl","const",[199,277,278],{"class":216}," DOMAIN ",[199,280,281],{"class":212},"=",[199,283,226],{"class":212},[199,285,157],{"class":229},[199,287,233],{"class":212},[199,289,236],{"class":212},[199,291,293,295,298,300,303,307,310,313,316,319,322,324,327,329,332],{"class":201,"line":292},5,[199,294,275],{"class":274},[199,296,297],{"class":216}," routeRegex ",[199,299,281],{"class":212},[199,301,302],{"class":212}," new",[199,304,306],{"class":305},"s2Zo4"," RegExp",[199,308,309],{"class":216},"(",[199,311,312],{"class":212},"`",[199,314,315],{"class":229},"^",[199,317,318],{"class":216},"\\\u002F",[199,320,321],{"class":229},"tenant",[199,323,318],{"class":216},[199,325,326],{"class":229},"?",[199,328,312],{"class":212},[199,330,331],{"class":216},")",[199,333,236],{"class":212},[199,335,337,339,342,344,347,351,354,357,359,362,365,367,370,372,375,378],{"class":201,"line":336},6,[199,338,275],{"class":274},[199,340,341],{"class":216}," isTenantRoute ",[199,343,281],{"class":212},[199,345,346],{"class":212}," (",[199,348,350],{"class":349},"sHdIc","route",[199,352,353],{"class":212},":",[199,355,248],{"class":356},"sBMFI",[199,358,331],{"class":212},[199,360,361],{"class":274}," =>",[199,363,364],{"class":216}," route",[199,366,150],{"class":212},[199,368,369],{"class":216},"path",[199,371,150],{"class":212},[199,373,374],{"class":305},"match",[199,376,377],{"class":216},"(routeRegex)",[199,379,236],{"class":212},[199,381,383],{"class":201,"line":382},7,[199,384,268],{"emptyLinePlaceholder":267},[199,386,388,391,394,397,400,403],{"class":201,"line":387},8,[199,389,390],{"class":205},"export",[199,392,393],{"class":205}," default",[199,395,396],{"class":216}," \u003C",[199,398,399],{"class":356},"RouterOptions",[199,401,402],{"class":216},">",[199,404,405],{"class":212},"{\n",[199,407,409,412,414,416,419,421,423],{"class":201,"line":408},9,[199,410,411],{"class":305},"  routes",[199,413,353],{"class":212},[199,415,346],{"class":212},[199,417,418],{"class":349},"routes",[199,420,331],{"class":212},[199,422,361],{"class":274},[199,424,425],{"class":212}," {\n",[199,427,429,432,434,437,439,442,445,449],{"class":201,"line":428},10,[199,430,431],{"class":274},"    const",[199,433,213],{"class":212},[199,435,436],{"class":216}," hostname",[199,438,220],{"class":212},[199,440,441],{"class":212}," =",[199,443,444],{"class":305}," useRequestURL",[199,446,448],{"class":447},"swJcz","()",[199,450,236],{"class":212},[199,452,454,456,459,461,463,465,468,470,472,474,477,480,483,486,489,491],{"class":201,"line":453},11,[199,455,431],{"class":274},[199,457,458],{"class":216}," subdomain",[199,460,441],{"class":212},[199,462,436],{"class":216},[199,464,150],{"class":212},[199,466,467],{"class":305},"replace",[199,469,309],{"class":447},[199,471,312],{"class":212},[199,473,150],{"class":229},[199,475,476],{"class":212},"${",[199,478,479],{"class":216},"DOMAIN",[199,481,482],{"class":212},"}`",[199,484,485],{"class":212},",",[199,487,488],{"class":212}," ''",[199,490,331],{"class":447},[199,492,236],{"class":212},[199,494,496],{"class":201,"line":495},12,[199,497,268],{"emptyLinePlaceholder":267},[199,499,501,504],{"class":201,"line":500},13,[199,502,503],{"class":205},"    return",[199,505,506],{"class":216}," routes\n",[199,508,510,513,516,518,520,522,524,526,528,531,534,537,539,541,544,546,549,552,554,556],{"class":201,"line":509},14,[199,511,512],{"class":212},"      .",[199,514,515],{"class":305},"filter",[199,517,309],{"class":447},[199,519,309],{"class":212},[199,521,350],{"class":349},[199,523,331],{"class":212},[199,525,361],{"class":274},[199,527,346],{"class":447},[199,529,530],{"class":216},"subdomain",[199,532,533],{"class":212}," ?",[199,535,536],{"class":305}," isTenantRoute",[199,538,309],{"class":447},[199,540,350],{"class":216},[199,542,543],{"class":447},") ",[199,545,353],{"class":212},[199,547,548],{"class":212}," !",[199,550,551],{"class":305},"isTenantRoute",[199,553,309],{"class":447},[199,555,350],{"class":216},[199,557,558],{"class":447},")))\n",[199,560,562,564,567,569,571,573,575,577,579],{"class":201,"line":561},15,[199,563,512],{"class":212},[199,565,566],{"class":305},"map",[199,568,309],{"class":447},[199,570,309],{"class":212},[199,572,350],{"class":349},[199,574,331],{"class":212},[199,576,361],{"class":274},[199,578,346],{"class":447},[199,580,405],{"class":212},[199,582,584,587,589],{"class":201,"line":583},16,[199,585,586],{"class":212},"        ...",[199,588,350],{"class":216},[199,590,591],{"class":212},",\n",[199,593,595,598,600,602,604,606,608,610,612,615,617,619,622,624,626],{"class":201,"line":594},17,[199,596,597],{"class":447},"        path",[199,599,353],{"class":212},[199,601,364],{"class":216},[199,603,150],{"class":212},[199,605,369],{"class":216},[199,607,150],{"class":212},[199,609,467],{"class":305},[199,611,309],{"class":447},[199,613,614],{"class":216},"routeRegex",[199,616,485],{"class":212},[199,618,226],{"class":212},[199,620,621],{"class":229},"\u002F",[199,623,233],{"class":212},[199,625,331],{"class":447},[199,627,591],{"class":212},[199,629,631,634,637],{"class":201,"line":630},18,[199,632,633],{"class":212},"      }",[199,635,636],{"class":447},"))",[199,638,236],{"class":212},[199,640,642],{"class":201,"line":641},19,[199,643,644],{"class":212},"  },\n",[199,646,648],{"class":201,"line":647},20,[199,649,650],{"class":212},"};\n",[10,652,653],{},[13,654,655,656,658,659,662,663,665,666,668,669,672,673,150],{},"In this example, we get the subdomain by removing the domain from the hostname. Then we filter which routes should be accessible based on if there is a subdomain. So, all pages in the ",[45,657,149],{}," folder will be accessible from ",[45,660,661],{},"*.my-company.com",", and all other pages will be accessible from ",[45,664,157],{},". Lastly, we remove the folder from the path so that the pages in the ",[45,667,149],{}," folder are accessible from ",[45,670,671],{},"*.my-company.com\u002Fsome-page"," instead of ",[45,674,675],{},"*.my-company.com\u002Ftenant\u002Fsome-page",[25,677,680],{":defaultExpanded":678,":items":679,"title":29},"[\"app\",\"pages\"]","[{\"title\":\"app\",\"children\":[{\"title\":\"pages\",\"children\":[{\"title\":\"tenant\",\"children\":[{\"title\":\"dashboard.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"},{\"title\":\"index.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"},{\"title\":\"tickets.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"}]},{\"title\":\"other\",\"children\":[{\"title\":\"page.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"}]},{\"title\":\"index.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"},{\"title\":\"pricing.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"}]},{\"title\":\"app.vue\",\"icon\":\"i-vscode-icons-file-type-vue\"},{\"title\":\"router.options.ts\",\"icon\":\"i-vscode-icons-file-type-typescript\"}]},{\"title\":\"nuxt.config.ts\",\"icon\":\"i-vscode-icons-file-type-nuxt\"},{\"title\":\"package.json\",\"icon\":\"i-vscode-icons-file-type-json\"}]",[31,681,682,685,721],{"v-slot:left":33},[10,683,684],{},"With the custom routing in place, we can now split the application into each part:",[38,686,687,695,703,710,718],{},[41,688,689,690,692,693],{},"Any page in ",[45,691,149],{}," will be accessible at ",[45,694,661],{},[41,696,689,697,692,700],{},[45,698,699],{},"pages\u002Fother",[45,701,702],{},"my-company.com\u002Fother",[41,704,705,692,708],{},[45,706,707],{},"pages\u002Findex.vue",[45,709,157],{},[41,711,712,692,715],{},[45,713,714],{},"pages\u002Fpricing.vue",[45,716,717],{},"my-company.com\u002Fpricing",[41,719,720],{},"etc.",[10,722,723,724,727,728,730],{},"Now when a user visits ",[45,725,726],{},"some-event.my-company.com",", all pages in ",[45,729,149],{}," will be accessible. Based on the subdomain, we can fetch all the cool tickets that are available for purchase from the database and display them on the page.",[93,732,734],{"id":733},"fetching-the-correct-data","Fetching the correct data",[10,736,737,738,740],{},"When visiting ",[45,739,726],{},", we don't want to fetch all the tickets from the database. We want to fetch the tickets for that specific event. A very simple way to do this is to create a composable that just stores the current tenant ID, or in my case the event ID.",[190,742,745],{"className":192,"code":743,"filename":744,"language":195,"meta":33,"style":33},"export function useEvent() {\n  return useStorage\u003Cstring>('eventID', () => '');\n}\n","app\u002Fcomposables\u002FuseEvent.ts",[45,746,747,761,799],{"__ignoreMap":33},[199,748,749,751,754,757,759],{"class":201,"line":202},[199,750,390],{"class":205},[199,752,753],{"class":274}," function",[199,755,756],{"class":305}," useEvent",[199,758,448],{"class":212},[199,760,425],{"class":212},[199,762,763,766,769,772,775,777,779,781,784,786,788,791,793,795,797],{"class":201,"line":239},[199,764,765],{"class":205},"  return",[199,767,768],{"class":305}," useStorage",[199,770,771],{"class":212},"\u003C",[199,773,774],{"class":356},"string",[199,776,402],{"class":212},[199,778,309],{"class":447},[199,780,233],{"class":212},[199,782,783],{"class":229},"eventID",[199,785,233],{"class":212},[199,787,485],{"class":212},[199,789,790],{"class":212}," ()",[199,792,361],{"class":274},[199,794,488],{"class":212},[199,796,331],{"class":447},[199,798,236],{"class":212},[199,800,801],{"class":201,"line":264},[199,802,803],{"class":212},"}\n",[10,805,806],{},"In the router options, we can now fetch the event ID from the subdomain and store it for easy access throughout the application.",[190,808,811],{"className":192,"code":809,"filename":194,"highlights":810,"language":195,"meta":33,"style":33},"import type { RouterOptions } from '@nuxt\u002Fschema';\nimport type { RouteRecordRaw } from 'vue-router';\n\nconst DOMAIN = 'my-company.com';\nconst routeRegex = new RegExp(`^\\\u002Ftenant\\\u002F?`);\nconst isTenantRoute = (route: RouteRecordRaw) => route.path.match(routeRegex);\n\nexport default \u003CRouterOptions>{\n  routes: (routes) => {\n    const { hostname } = useRequestURL();\n    const eventID = useEvent();\n\n    const subdomain = hostname.replace(`.${DOMAIN}`, '');\n    eventID.value = await $fetch('\u002Fapi\u002Ftenant', { query: { subdomain } });\n\n    return routes\n      .filter((route) => (eventID.value ? isTenantRoute(route) : !isTenantRoute(route)))\n      .map((route) => ({\n        ...route,\n        path: route.path.replace(routeRegex, '\u002F'),\n      }));\n  },\n};\n",[453,509,594],[45,812,813,835,857,861,877,909,943,947,961,977,995,1012,1016,1050,1099,1103,1109,1156,1176,1184,1216,1225,1230],{"__ignoreMap":33},[199,814,815,817,819,821,823,825,827,829,831,833],{"class":201,"line":202},[199,816,206],{"class":205},[199,818,209],{"class":205},[199,820,213],{"class":212},[199,822,217],{"class":216},[199,824,220],{"class":212},[199,826,223],{"class":205},[199,828,226],{"class":212},[199,830,230],{"class":229},[199,832,233],{"class":212},[199,834,236],{"class":212},[199,836,837,839,841,843,845,847,849,851,853,855],{"class":201,"line":239},[199,838,206],{"class":205},[199,840,209],{"class":205},[199,842,213],{"class":212},[199,844,248],{"class":216},[199,846,220],{"class":212},[199,848,223],{"class":205},[199,850,226],{"class":212},[199,852,257],{"class":229},[199,854,233],{"class":212},[199,856,236],{"class":212},[199,858,859],{"class":201,"line":264},[199,860,268],{"emptyLinePlaceholder":267},[199,862,863,865,867,869,871,873,875],{"class":201,"line":271},[199,864,275],{"class":274},[199,866,278],{"class":216},[199,868,281],{"class":212},[199,870,226],{"class":212},[199,872,157],{"class":229},[199,874,233],{"class":212},[199,876,236],{"class":212},[199,878,879,881,883,885,887,889,891,893,895,897,899,901,903,905,907],{"class":201,"line":292},[199,880,275],{"class":274},[199,882,297],{"class":216},[199,884,281],{"class":212},[199,886,302],{"class":212},[199,888,306],{"class":305},[199,890,309],{"class":216},[199,892,312],{"class":212},[199,894,315],{"class":229},[199,896,318],{"class":216},[199,898,321],{"class":229},[199,900,318],{"class":216},[199,902,326],{"class":229},[199,904,312],{"class":212},[199,906,331],{"class":216},[199,908,236],{"class":212},[199,910,911,913,915,917,919,921,923,925,927,929,931,933,935,937,939,941],{"class":201,"line":336},[199,912,275],{"class":274},[199,914,341],{"class":216},[199,916,281],{"class":212},[199,918,346],{"class":212},[199,920,350],{"class":349},[199,922,353],{"class":212},[199,924,248],{"class":356},[199,926,331],{"class":212},[199,928,361],{"class":274},[199,930,364],{"class":216},[199,932,150],{"class":212},[199,934,369],{"class":216},[199,936,150],{"class":212},[199,938,374],{"class":305},[199,940,377],{"class":216},[199,942,236],{"class":212},[199,944,945],{"class":201,"line":382},[199,946,268],{"emptyLinePlaceholder":267},[199,948,949,951,953,955,957,959],{"class":201,"line":387},[199,950,390],{"class":205},[199,952,393],{"class":205},[199,954,396],{"class":216},[199,956,399],{"class":356},[199,958,402],{"class":216},[199,960,405],{"class":212},[199,962,963,965,967,969,971,973,975],{"class":201,"line":408},[199,964,411],{"class":305},[199,966,353],{"class":212},[199,968,346],{"class":212},[199,970,418],{"class":349},[199,972,331],{"class":212},[199,974,361],{"class":274},[199,976,425],{"class":212},[199,978,979,981,983,985,987,989,991,993],{"class":201,"line":428},[199,980,431],{"class":274},[199,982,213],{"class":212},[199,984,436],{"class":216},[199,986,220],{"class":212},[199,988,441],{"class":212},[199,990,444],{"class":305},[199,992,448],{"class":447},[199,994,236],{"class":212},[199,996,999,1001,1004,1006,1008,1010],{"class":997,"line":453},[201,998],"highlight",[199,1000,431],{"class":274},[199,1002,1003],{"class":216}," eventID",[199,1005,441],{"class":212},[199,1007,756],{"class":305},[199,1009,448],{"class":447},[199,1011,236],{"class":212},[199,1013,1014],{"class":201,"line":495},[199,1015,268],{"emptyLinePlaceholder":267},[199,1017,1018,1020,1022,1024,1026,1028,1030,1032,1034,1036,1038,1040,1042,1044,1046,1048],{"class":201,"line":500},[199,1019,431],{"class":274},[199,1021,458],{"class":216},[199,1023,441],{"class":212},[199,1025,436],{"class":216},[199,1027,150],{"class":212},[199,1029,467],{"class":305},[199,1031,309],{"class":447},[199,1033,312],{"class":212},[199,1035,150],{"class":229},[199,1037,476],{"class":212},[199,1039,479],{"class":216},[199,1041,482],{"class":212},[199,1043,485],{"class":212},[199,1045,488],{"class":212},[199,1047,331],{"class":447},[199,1049,236],{"class":212},[199,1051,1053,1056,1058,1061,1063,1066,1069,1071,1073,1076,1078,1080,1082,1085,1087,1089,1091,1093,1095,1097],{"class":1052,"line":509},[201,998],[199,1054,1055],{"class":216},"    eventID",[199,1057,150],{"class":212},[199,1059,1060],{"class":216},"value",[199,1062,441],{"class":212},[199,1064,1065],{"class":205}," await",[199,1067,1068],{"class":305}," $fetch",[199,1070,309],{"class":447},[199,1072,233],{"class":212},[199,1074,1075],{"class":229},"\u002Fapi\u002Ftenant",[199,1077,233],{"class":212},[199,1079,485],{"class":212},[199,1081,213],{"class":212},[199,1083,1084],{"class":447}," query",[199,1086,353],{"class":212},[199,1088,213],{"class":212},[199,1090,458],{"class":216},[199,1092,220],{"class":212},[199,1094,220],{"class":212},[199,1096,331],{"class":447},[199,1098,236],{"class":212},[199,1100,1101],{"class":201,"line":561},[199,1102,268],{"emptyLinePlaceholder":267},[199,1104,1105,1107],{"class":201,"line":583},[199,1106,503],{"class":205},[199,1108,506],{"class":216},[199,1110,1112,1114,1116,1118,1120,1122,1124,1126,1128,1130,1132,1134,1136,1138,1140,1142,1144,1146,1148,1150,1152,1154],{"class":1111,"line":594},[201,998],[199,1113,512],{"class":212},[199,1115,515],{"class":305},[199,1117,309],{"class":447},[199,1119,309],{"class":212},[199,1121,350],{"class":349},[199,1123,331],{"class":212},[199,1125,361],{"class":274},[199,1127,346],{"class":447},[199,1129,783],{"class":216},[199,1131,150],{"class":212},[199,1133,1060],{"class":216},[199,1135,533],{"class":212},[199,1137,536],{"class":305},[199,1139,309],{"class":447},[199,1141,350],{"class":216},[199,1143,543],{"class":447},[199,1145,353],{"class":212},[199,1147,548],{"class":212},[199,1149,551],{"class":305},[199,1151,309],{"class":447},[199,1153,350],{"class":216},[199,1155,558],{"class":447},[199,1157,1158,1160,1162,1164,1166,1168,1170,1172,1174],{"class":201,"line":630},[199,1159,512],{"class":212},[199,1161,566],{"class":305},[199,1163,309],{"class":447},[199,1165,309],{"class":212},[199,1167,350],{"class":349},[199,1169,331],{"class":212},[199,1171,361],{"class":274},[199,1173,346],{"class":447},[199,1175,405],{"class":212},[199,1177,1178,1180,1182],{"class":201,"line":641},[199,1179,586],{"class":212},[199,1181,350],{"class":216},[199,1183,591],{"class":212},[199,1185,1186,1188,1190,1192,1194,1196,1198,1200,1202,1204,1206,1208,1210,1212,1214],{"class":201,"line":647},[199,1187,597],{"class":447},[199,1189,353],{"class":212},[199,1191,364],{"class":216},[199,1193,150],{"class":212},[199,1195,369],{"class":216},[199,1197,150],{"class":212},[199,1199,467],{"class":305},[199,1201,309],{"class":447},[199,1203,614],{"class":216},[199,1205,485],{"class":212},[199,1207,226],{"class":212},[199,1209,621],{"class":229},[199,1211,233],{"class":212},[199,1213,331],{"class":447},[199,1215,591],{"class":212},[199,1217,1219,1221,1223],{"class":201,"line":1218},21,[199,1220,633],{"class":212},[199,1222,636],{"class":447},[199,1224,236],{"class":212},[199,1226,1228],{"class":201,"line":1227},22,[199,1229,644],{"class":212},[199,1231,1233],{"class":201,"line":1232},23,[199,1234,650],{"class":212},[10,1236,1237],{},[13,1238,1239,1240,1243],{},"The highlighted lines show the changes. Before filtering the routes, we fetch the event ID based on the subdomain from a database, KV, or whatever you prefer. If the subdomain is not related to an event, we can show the normal pages instead. This is the behaviour we want for ",[45,1241,1242],{},"www.my-company.com"," for example.",[10,1245,1246],{},"Now we can fetch the data for the event and display it on the page.",[190,1248,1253],{"className":1249,"code":1250,"filename":1251,"language":1252,"meta":33,"style":33},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Cscript setup lang=\"ts\">\nconst eventID = useEvent();\nconst { data } = await useFetch(`\u002Fapi\u002Fevent\u002F${eventID}`);\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Cpre>{{ data }}\u003C\u002Fpre>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n","pages\u002Ftenant\u002Findex.vue","vue",[45,1254,1255,1280,1295,1331,1340,1344,1352,1362,1380,1389],{"__ignoreMap":33},[199,1256,1257,1259,1262,1265,1268,1270,1273,1275,1277],{"class":201,"line":202},[199,1258,771],{"class":212},[199,1260,1261],{"class":447},"script",[199,1263,1264],{"class":274}," setup",[199,1266,1267],{"class":274}," lang",[199,1269,281],{"class":212},[199,1271,1272],{"class":212},"\"",[199,1274,195],{"class":229},[199,1276,1272],{"class":212},[199,1278,1279],{"class":212},">\n",[199,1281,1282,1284,1287,1289,1291,1293],{"class":201,"line":239},[199,1283,275],{"class":274},[199,1285,1286],{"class":216}," eventID ",[199,1288,281],{"class":212},[199,1290,756],{"class":305},[199,1292,448],{"class":216},[199,1294,236],{"class":212},[199,1296,1297,1299,1301,1304,1307,1309,1311,1314,1316,1318,1321,1323,1325,1327,1329],{"class":201,"line":264},[199,1298,275],{"class":274},[199,1300,213],{"class":212},[199,1302,1303],{"class":216}," data ",[199,1305,1306],{"class":212},"}",[199,1308,441],{"class":212},[199,1310,1065],{"class":205},[199,1312,1313],{"class":305}," useFetch",[199,1315,309],{"class":216},[199,1317,312],{"class":212},[199,1319,1320],{"class":229},"\u002Fapi\u002Fevent\u002F",[199,1322,476],{"class":212},[199,1324,783],{"class":216},[199,1326,482],{"class":212},[199,1328,331],{"class":216},[199,1330,236],{"class":212},[199,1332,1333,1336,1338],{"class":201,"line":271},[199,1334,1335],{"class":212},"\u003C\u002F",[199,1337,1261],{"class":447},[199,1339,1279],{"class":212},[199,1341,1342],{"class":201,"line":292},[199,1343,268],{"emptyLinePlaceholder":267},[199,1345,1346,1348,1350],{"class":201,"line":336},[199,1347,771],{"class":212},[199,1349,31],{"class":447},[199,1351,1279],{"class":212},[199,1353,1354,1357,1360],{"class":201,"line":382},[199,1355,1356],{"class":212},"  \u003C",[199,1358,1359],{"class":447},"div",[199,1361,1279],{"class":212},[199,1363,1364,1367,1369,1371,1374,1376,1378],{"class":201,"line":387},[199,1365,1366],{"class":212},"    \u003C",[199,1368,190],{"class":447},[199,1370,402],{"class":212},[199,1372,1373],{"class":216},"{{ data }}",[199,1375,1335],{"class":212},[199,1377,190],{"class":447},[199,1379,1279],{"class":212},[199,1381,1382,1385,1387],{"class":201,"line":408},[199,1383,1384],{"class":212},"  \u003C\u002F",[199,1386,1359],{"class":447},[199,1388,1279],{"class":212},[199,1390,1391,1393,1395],{"class":201,"line":428},[199,1392,1335],{"class":212},[199,1394,31],{"class":447},[199,1396,1279],{"class":212},[93,1398,1400],{"id":1399},"adding-dns-records","Adding DNS Records",[10,1402,1403,1404,1407],{},"The last step before the everything works, we need to add the subdomain records to the DNS. In this case we want to allow for any subdomain so we need to add a wildcard DNS record. I'm deploying to Cloudflare Pages so the actual location of the application is at ",[45,1405,1406],{},"\u003Cproject-name>.\u003Corg-name>.pages.dev",". First I need to point to that domain with the following DNS records:",[1409,1410,1411,1430],"table",{},[1412,1413,1414],"thead",{},[1415,1416,1417,1421,1424,1427],"tr",{},[1418,1419,1420],"th",{},"Type",[1418,1422,1423],{},"Name",[1418,1425,1426],{},"Value",[1418,1428,1429],{},"Notes",[1431,1432,1433,1451],"tbody",{},[1415,1434,1435,1439,1444,1448],{},[1436,1437,1438],"td",{},"CNAME",[1436,1440,1441],{},[45,1442,1443],{},"@",[1436,1445,1446],{},[45,1447,1406],{},[1436,1449,1450],{},"root domain",[1415,1452,1453,1455,1460,1464],{},[1436,1454,1438],{},[1436,1456,1457],{},[45,1458,1459],{},"*",[1436,1461,1462],{},[45,1463,1406],{},[1436,1465,1466],{},"wildcard subdomain",[10,1468,1469,1470,1472],{},"And then also add the domains directly to the Cloudflare Pages project as a \"Pages Domain\". The root domain works just as expected. All pages are accessible except the ones in ",[45,1471,149],{},". The wildcard subdomain however...",[17,1474,1476],{"id":1475},"the-limits-of-cloudflare-pages","The limits of Cloudflare Pages",[10,1478,1479,1480,1482,1483,1485],{},"Here is where all the problems start. Wildcard domains are not supported in Cloudflare Pages. If you add a the ",[45,1481,661],{}," domain it will read it as the actual character ",[45,1484,1459],{}," instead of a wildcard. Oh no... As an alternative, I could add a DNS record and Pages Domain for each event, but there is a limit to that. Free plans only allow for 100 Pages Domains per project. Enough for the single client I am working with, but not for the future.",[10,1487,1488],{},"A few things to make clear about my Cloudflare setup:",[38,1490,1491,1494,1497],{},[41,1492,1493],{},"I'm using NuxtHub to deploy this project to Cloudflare Pages",[41,1495,1496],{},"My domain is registered at the Cloudflare Registrar and is using the Free plan",[41,1498,1499],{},"I have a Cloudflare Workers Paid subscription",[10,1501,1502,1503,1506,1507,1510,1511,1513],{},"With NuxtHub, each project can claim a free ",[45,1504,1505],{},"nuxt.dev"," domain at which your site is accessible. I asked ",[109,1508],{"username":1509},"atinux"," how he achieved this. It turns out that for each ",[45,1512,1505],{}," domain, a DNS record is created and the Pages Domain is added to the user's own account. The only limitation in this case is the DNS record limit and keeping it up-to-date. I knew he is working on re-writing NuxtHub to support Cloudflare Workers because of the limitations with Pages. During our conversation he gave me info about some of the changes coming to NuxtHub and in the process I learned about Cloudflare's 'Workers for SaaS' offering (important for later). I concluded that I had to deploy to Cloudflare Workers instead of Pages. Workers supports domains just like Pages, but also has something called 'Workers Routes'.",[17,1515,1517],{"id":1516},"moving-to-cloudflare-workers","Moving to Cloudflare Workers",[10,1519,1520,1521,1524,1525,1528,1529,1532,1533,1537],{},"First things first, I somehow need to make the application compatible with Cloudflare Workers. Luckily with Nuxt, it is as simple as changing the preset in the ",[45,1522,1523],{},"nuxt.config.ts"," to ",[45,1526,1527],{},"cloudflare-module"," and I'm good to go! Not so fast. Normally this would be the case, but I'm deploying with NuxtHub which is only compatible with Pages. Besides some changes to the code, I need to create a ",[45,1530,1531],{},"wrangler.toml"," file in the root of the project. This would normally be generated by ",[1534,1535],"package",{"name":1536},"@nuxthub\u002Fcore",", but now I need to create it manually.",[93,1539,1541],{"id":1540},"wrangler-configuration","Wrangler configuration",[190,1543,1547],{"className":1544,"code":1545,"filename":1531,"language":1546,"meta":33,"style":33},"language-toml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","name = \"my-project\"\ncompatibility_date = \"2025-01-01\"\ncompatibility_flags = [\"nodejs_compat\"]\n\nmain = \".\u002F.output\u002Fserver\u002Findex.mjs\"\nassets = { directory = \".\u002F.output\u002Fpublic\u002F\", binding = \"ASSETS\" }\n\nroutes = [{ pattern = \"*.my-company.com\u002F*\", zone_name = \"my-company.com\" }]\n","toml",[45,1548,1549,1554,1559,1564,1568,1573,1578,1582],{"__ignoreMap":33},[199,1550,1551],{"class":201,"line":202},[199,1552,1553],{},"name = \"my-project\"\n",[199,1555,1556],{"class":201,"line":239},[199,1557,1558],{},"compatibility_date = \"2025-01-01\"\n",[199,1560,1561],{"class":201,"line":264},[199,1562,1563],{},"compatibility_flags = [\"nodejs_compat\"]\n",[199,1565,1566],{"class":201,"line":271},[199,1567,268],{"emptyLinePlaceholder":267},[199,1569,1570],{"class":201,"line":292},[199,1571,1572],{},"main = \".\u002F.output\u002Fserver\u002Findex.mjs\"\n",[199,1574,1575],{"class":201,"line":336},[199,1576,1577],{},"assets = { directory = \".\u002F.output\u002Fpublic\u002F\", binding = \"ASSETS\" }\n",[199,1579,1580],{"class":201,"line":382},[199,1581,268],{"emptyLinePlaceholder":267},[199,1583,1584],{"class":201,"line":387},[199,1585,1586],{},"routes = [{ pattern = \"*.my-company.com\u002F*\", zone_name = \"my-company.com\" }]\n",[10,1588,99,1589,1591,1592,1594,1595,1597,1598,1600],{},[45,1590,418],{}," array is where the magic happens. You can define URL patterns which will trigger the execution of a specific worker. Not only for the domain, but also for the route. In hindsight, this is super obvious — it's called a ",[13,1593,350],{}," after all — but at the time I didn't think of it. I added the route pattern ",[45,1596,661],{}," to the ",[45,1599,418],{}," array and consequentially banged my head against the wall for far too long because it just didn't work. The subdomain loaded fine, but the routes were not accessible. So make sure to add the wildcard on the route as well!",[93,1602,1604],{"id":1603},"updating-dns-records","Updating DNS records",[10,1606,1607],{},"We need to update the DNS records to point to the workers project instead of the Pages project. First, remove any records related to the Pages project. Then, add your domain as a Workers Domain in the specific worker's settings. Lastly, in the DNS settings you can add a wildcard subdomain to the domain and it will look something like this:",[1409,1609,1610,1622],{},[1412,1611,1612],{},[1415,1613,1614,1616,1618,1620],{},[1418,1615,1420],{},[1418,1617,1423],{},[1418,1619,1426],{},[1418,1621,1429],{},[1431,1623,1624,1643],{},[1415,1625,1626,1631,1635,1640],{},[1436,1627,1628],{},[45,1629,1630],{},"Worker",[1436,1632,1633],{},[45,1634,157],{},[1436,1636,1637],{},[45,1638,1639],{},"my-project",[1436,1641,1642],{},"root domain (worker domain)",[1415,1644,1645,1649,1653,1657],{},[1436,1646,1647],{},[45,1648,1438],{},[1436,1650,1651],{},[45,1652,1459],{},[1436,1654,1655],{},[45,1656,157],{},[1436,1658,1466],{},[93,1660,1662],{"id":1661},"turning-off-specific-subdomains","Turning off specific subdomains",[10,1664,1665,1666,1669],{},"In my case, I want some subdomains to point to a server outside of Cloudflare. This is a websocket server that I run on a VPS. Say I want to use ",[45,1667,1668],{},"ws.my-company.com"," to point to the external server, adding the specific subdomain to the DNS does not work immediately.",[1409,1671,1672,1684],{},[1412,1673,1674],{},[1415,1675,1676,1678,1680,1682],{},[1418,1677,1420],{},[1418,1679,1423],{},[1418,1681,1426],{},[1418,1683,1429],{},[1431,1685,1686,1702,1718],{},[1415,1687,1688,1692,1696,1700],{},[1436,1689,1690],{},[45,1691,1630],{},[1436,1693,1694],{},[45,1695,157],{},[1436,1697,1698],{},[45,1699,1639],{},[1436,1701,1642],{},[1415,1703,1704,1708,1712,1716],{},[1436,1705,1706],{},[45,1707,1438],{},[1436,1709,1710],{},[45,1711,1459],{},[1436,1713,1714],{},[45,1715,157],{},[1436,1717,1466],{},[1415,1719,1720,1725,1730,1735],{},[1436,1721,1722],{},[45,1723,1724],{},"A",[1436,1726,1727],{},[45,1728,1729],{},"ws",[1436,1731,1732],{},[45,1733,1734],{},"192.168.1.1",[1436,1736,1737],{},"VPS",[10,1739,1740,1741,226,1743,1747,1748,1751,1752,1755],{},"This will not work. The subdomain will trigger the worker and not route to the VPS. To make it work, you need to go to the ",[45,1742,157],{},[1744,1745,1746],"strong",{},"website"," settings' in the Cloudflare dashboard — not the worker settings. Then, under the 'Workers Routes' tab, you need to add a new route ",[45,1749,1750],{},"ws.my-company.com\u002F*"," and set the Worker to ",[13,1753,1754],{},"None",". Now, the subdomain will point to the external server instead of the worker.",[17,1757,1759],{"id":1758},"conclusion","Conclusion",[10,1761,1762,1763,150],{},"Adding subdomains to your application is a nice way to make your application feel more professional and trustworthy. It gives a sense of separation between different users. The support for subdomains in Cloudflare is certainly not perfect. And navigating the Cloudflare documentation and dashboards is not intuitive. The documentation is minimal and I only was able to solve the issues by going back and forth between AI and the documentation. I hope this blog post will save you some of the time and frustration I went through. An example project is available on ",[101,1764,1767],{"href":1765,"rel":1766},"https:\u002F\u002Fgithub.com\u002Fgerbuuun\u002Fnuxt-cloudflare-multi-tenancy",[105],"Github",[93,1769,1771],{"id":1770},"whats-next","What's next?",[10,1773,1774,1775,1778,1779,1782,1783,1786],{},"As I mentioned briefly earlier, during the migration I stumbled upon Cloudflare's 'Workers for SaaS' offering. This allows your clients to use their own domains to point to your worker project. Instead of using ",[45,1776,1777],{},"{my_event}.my-company.com",", they can use for example ",[45,1780,1781],{},"{my_event}.client-1.com"," or even just ",[45,1784,1785],{},"client-1.com",". This is an even better solution than subdomains. After validating the subdomain routing, I will be looking into this and see if I can implement it within the free tier. And if it works, I will write a blog post about it as well.",[10,1788,1789],{},[13,1790,1791],{},"This is my very first blog post about my development journey. Lots of people I follow in this space said to just publish your work and share it with the world. Well, here it is. I hope you enjoyed reading it and learned something from it.",[1793,1794,1795],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":33,"searchDepth":239,"depth":239,"links":1797},[1798,1799,1804,1805,1810],{"id":19,"depth":239,"text":20},{"id":80,"depth":239,"text":81,"children":1800},[1801,1802,1803],{"id":95,"depth":264,"text":96},{"id":733,"depth":264,"text":734},{"id":1399,"depth":264,"text":1400},{"id":1475,"depth":239,"text":1476},{"id":1516,"depth":239,"text":1517,"children":1806},[1807,1808,1809],{"id":1540,"depth":264,"text":1541},{"id":1603,"depth":264,"text":1604},{"id":1661,"depth":264,"text":1662},{"id":1758,"depth":239,"text":1759,"children":1811},[1812],{"id":1770,"depth":264,"text":1771},"2025-01-04","My journey implementing multi-tenancy in my Nuxt project deployed to cloudflare workers.","md",null,{},"\u002Fblog\u002Fmulti-tenancy-with-nuxt-and-cloudflare",[1820,1823,1825,1827,1831,1834,1838,1841,1844],{"label":1821,"to":103,"icon":1822},"2024 Multi-Tenancy blog","i-lucide-letter-text",{"label":1824,"to":119,"icon":1822},"Why Keypress blog",{"label":127,"to":125,"icon":1826},"i-simple-icons-github",{"label":1828,"to":1829,"icon":1830},"Cloudflare DNS Wildcard","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fdns\u002Fmanage-dns-records\u002Freference\u002Fwildcard-dns-records\u002F","i-simple-icons-cloudflare",{"label":1832,"to":1833,"icon":1830},"Cloudflare Worker Routes","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers\u002Fconfiguration\u002Frouting\u002F",{"label":1835,"to":1836,"icon":1837},"NuxtHub","https:\u002F\u002Fhub.nuxt.com\u002F","i-simple-icons-nuxtdotjs",{"label":1839,"to":1840,"icon":1837},"Nuxt Custom Routing","https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fguide\u002Frecipes\u002Fcustom-routing",{"label":1842,"to":139,"icon":1843},"Vue Router Options","i-simple-icons-vuedotjs",{"label":1845,"to":1765,"icon":1826},"Example project",{"title":5,"description":1814},{"loc":1818},"blog\u002Fmulti-tenancy-with-nuxt-and-cloudflare",[1850,1851],"Cloudflare","Nuxt","XR1WuNFBsFiumNJ_skqBav0ySs7HtQGrkqeM3o2zYrc",[1816,1816],{"user":1855},{"id":1856,"username":111,"name":1857,"twitter":1858,"avatar":1859},8784712,"EGOIST","localhost_5173","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F8784712?v=4",{"user":1861},{"id":1862,"username":115,"name":115,"twitter":115,"avatar":1863},59365435,"https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F59365435?v=4",{"user":1865},{"id":1866,"username":1509,"name":1867,"twitter":1509,"avatar":1868},904724,"Sébastien Chopin","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F904724?v=4",{"name":1536,"private":1870,"version":1871,"description":1872,"repository":1873,"homepage":1876,"license":1877,"type":1878,"exports":1879,"main":1882,"typesVersions":1894,"bin":1897,"keywords":1900,"scripts":1908,"dependencies":1923,"devDependencies":1949,"_id":1965,"gitHead":1966,"bugs":1967,"_nodeVersion":1969,"_npmVersion":1970,"dist":1971,"_npmUser":1981,"directories":1983,"maintainers":1984,"_npmOperationalInternal":1995,"_hasShrinkwrap":1870},false,"0.10.7","Build full-stack Nuxt applications, with zero configuration.",{"type":1874,"url":1875},"git","git+https:\u002F\u002Fgithub.com\u002Fnuxt-hub\u002Fcore.git","https:\u002F\u002Fhub.nuxt.com","Apache-2.0","module",{".":1880,".\u002Fblob":1883,".\u002Fblob\u002Fdrivers\u002F*":1886,".\u002Fdb":1889,".\u002Fkv":1892},{"types":1881,"import":1882},".\u002Fdist\u002Ftypes.d.mts",".\u002Fdist\u002Fmodule.mjs",{"types":1884,"import":1885},".\u002Fdist\u002Fblob\u002Flib\u002Findex.d.ts",".\u002Fdist\u002Fblob\u002Flib\u002Findex.mjs",{"types":1887,"import":1888},".\u002Fdist\u002Fblob\u002Flib\u002Fdrivers\u002F*.d.ts",".\u002Fdist\u002Fblob\u002Flib\u002Fdrivers\u002F*.mjs",{"types":1890,"import":1891},".\u002Fdist\u002Fdb\u002Flib\u002Findex.d.ts",".\u002Fdist\u002Fdb\u002Flib\u002Findex.mjs",{"types":1893},".\u002Fdist\u002Fkv\u002Fruntime\u002Fkv.d.ts",{"*":1895},{".":1896},[1881],{"nuxt-hub":1898,"nuxt-db":1899},"cli\u002Fbin\u002Fnuxthub.mjs","cli\u002Fbin\u002Fnuxt-db.mjs",[1901,1902,1903,1904,1905,1906,1907],"nuxt","fullstack","cloudflare","vercel","database","kv","blob",{"prepack":1909,"dev:prepare":1910,"dev":1911,"dev:build":1912,"docs":1913,"docs:build":1914,"release":1915,"lint":1916,"lint:fix":1917,"test":1918,"test:watch":1919,"test:types":1920,"hub":1921,"db":1922},"nuxt-module-build build","nuxt-module-build build --stub && mkdist --src=src\u002Fblob\u002Flib --dist=dist\u002Fblob\u002Flib -d && mkdist --src=src\u002Fdb\u002Flib --dist=dist\u002Fdb\u002Flib -d && nuxt-module-build prepare && nuxi prepare playground","nuxi dev playground","nuxi build playground","PORT=4000 nuxi dev docs","nuxi generate docs","npm run lint && npm run test && changelogen --release && npm publish && git push --follow-tags","eslint .","eslint . --fix","vitest run --test-timeout=60000","vitest watch","vue-tsc --noEmit && cd playground && vue-tsc --noEmit",".\u002Fcli\u002Fbin\u002Fnuxthub.mjs",".\u002Fcli\u002Fbin\u002Fnuxt-db.mjs",{"@cloudflare\u002Fworkers-types":1924,"@nuxt\u002Fkit":1925,"@uploadthing\u002Fmime-types":1926,"c12":1927,"chokidar":1928,"citty":1929,"consola":1930,"defu":1931,"execa":1932,"get-port-please":1933,"h3":1934,"hookable":1935,"mime":1936,"nypm":1937,"ofetch":1938,"pathe":1939,"pkg-types":1940,"scule":1941,"std-env":1942,"tinyglobby":1943,"tsdown":1944,"ufo":1945,"uncrypto":1946,"unstorage":1947,"zod":1948},"^4.20260214.0","^4.3.1","^0.3.6","^3.3.3","^5.0.0","^0.2.1","^3.4.2","^6.1.4","^9.6.1","^3.2.0","^1.15.4","6.0.1","^4.1.0","^0.6.5","^1.5.1","^2.0.3","^2.3.0","^1.3.0","^3.10.0","^0.2.15","^0.18.1","^1.6.1","^0.1.3","^1.17.3","^4.2.1",{"@nuxt\u002Fdevtools":1950,"@nuxt\u002Feslint-config":1951,"@nuxt\u002Fmodule-builder":1952,"@nuxt\u002Fschema":1925,"@nuxt\u002Ftest-utils":1953,"@nuxthub\u002Fcore":1954,"@types\u002Fnode":1955,"better-sqlite3":1956,"changelogen":1957,"destr":1958,"eslint":1959,"jiti":1960,"nitropack":1961,"nuxt":1925,"typescript":1962,"vitest":1963,"vue-tsc":1964},"^3.1.1","^1.15.1","^1.0.2","3.21.0","link:","^25.2.3","^12.6.2","^0.6.2","^2.0.5","^9.39.2","^2.6.1","^2.13.1","^5.9.3","^4.0.18","^3.2.4","@nuxthub\u002Fcore@0.10.7","b071bec8c8b598be4f4d002f30e714493468e850",{"url":1968},"https:\u002F\u002Fgithub.com\u002Fnuxt-hub\u002Fcore\u002Fissues","22.22.0","10.9.4",{"integrity":1972,"shasum":1973,"tarball":1974,"fileCount":1975,"unpackedSize":1976,"signatures":1977},"sha512-L8GNzGE081udFIPyy3v8xlr8Cv6fijkEkvguB+Cg+0bMQiJSlzyay8FQyPDggXmn2ZcZLLCN3Z+VbTa9Vt93eQ==","bee40f8f5a3c6f7e7e0479206c463ebe4bc508bb","https:\u002F\u002Fregistry.npmjs.org\u002F@nuxthub\u002Fcore\u002F-\u002Fcore-0.10.7.tgz",63,185564,[1978],{"keyid":1979,"sig":1980},"SHA256:DhQ8wR5APBvFHLF\u002F+Tc+AYvPOdTpcIDqOhxsBHRwC7U","MEQCIHi\u002FUq9UnK\u002FNuUZmafu4U1zz7jiGhsxBOEeDwQfKhazuAiBgRVlmdqDQxOHSaBeDqup7d92JaiukgHAWRwT6sLL+8g==",{"name":1509,"email":1982},"atinux@gmail.com",{},[1985,1986,1989,1992],{"name":1509,"email":1982},{"name":1987,"email":1988},"pi0","pyapar@gmail.com",{"name":1990,"email":1991},"farnabaz","farnabaz@gmail.com",{"name":1993,"email":1994},"rihanarfan","rihan554@gmail.com",{"host":1996,"tmp":1997},"s3:\u002F\u002Fnpm-registry-packages-npm-production","tmp\u002Fcore_0.10.7_1772549087652_0.587692925982999",1783544160112]