Vercel Skew Protection with Vite

Vercel announced a new feature to mitigate errors due to version skew. Their blog post goes into the nature of the problem and the solution, but only mentions Nextjs. This is a problem I have also faced while working with Solidjs using SolidStart.

I suspected (hoped) this might be a platform feature with the direction of Vercel’s Build Output API and not something specific to Nextjs.

Looking into Nextjs

Trying to identify how it was implemented in Nextjs I searched for useDeploymentId in the Nextjs Github Repo. Investigating the git history I found the commits that added this functionality.

Looks like just adding the deployment id via ?dpl= query param or x-deployment-id header to asset requests was sufficient, indicating that this is a platform feature.

Trying it out

Before trying to implement this in SolidStart, I wanted to see if this would work outside of Nextjs.

I made a simple SolidStart app with a dynamic import and deployed it to Vercel.

Page with dynamic import
// data.ts
export const data = {
now: Date.now(),
version: "1", // deploy, change version and redeploy
}
// Page index.tsx
<button
onClick={() => {
import('./data').then((mod) => {
console.log('loaded data', mod.data)
})
}}
>
load dynamic data ({import.meta.env.VITE_VERCEL_DEPLOYMENT_ID})
</button>

Lets try out if specifying the header allows us to target specific deployments. I changed the data and deployed the new version, noting the filenames from the network tab.

Note: To get the deployment id I viewed the deployment on the Vercel dashboard.

Terminal window
# v1 - dpl_GntXEvUxWfFv3SWK3XYjE7EtPf6f
https://solid-vercel-skew-ryoid.vercel.app/assets/data-1b6755f5.js
# v2 - dpl_26vPJfjuTRSzvRf2yWfLNNTrhW9w
https://solid-vercel-skew-ryoid.vercel.app/assets/data-e597730f.js
# 404
curl 'https://solid-vercel-skew-ryoid.vercel.app/assets/data-1b6755f5.js'
# Returns our old version!
curl 'https://solid-vercel-skew-ryoid.vercel.app/assets/data-1b6755f5.js' \
--header 'x-deployment-id: dpl_GntXEvUxWfFv3SWK3XYjE7EtPf6f'

After deploying v2 trying to fetch the v1 asset returns 404 as expected.

Adding the deployment header x-deployment-id we hit our old deployment and get our data! Now that we know it works outside of Nextjs, we just need to make this automatic for all our assets. Obtaining the deployment id programatically to test this theory would be the start.


Other findings

While undocumented, the Nextjs implementation of adding dpl search param also worked.

Terminal window
curl '.../assets/data-1b6755f5.js?dpl=dpl_GntXEvUxWfFv3SWK3XYjE7EtPf6f'

Getting the Vercel Deployment Id

Vercel exposes a bunch of System Environment Variables. Nextjs has NEXT_DEPLOYMENT_ID although not listed on the env variables page probably due to its experimental nature it is mentioned in Skew Protection. Unfortunately with SolidStart it was not provided.

Trying to figure out how I could get this I found the Deployment API that I could call with the deployment host exposed through VITE_VERCEL_URL - the current deployment’s host. This is not ideal though since we need to make a network request.

fetch(`https://api.vercel.com/v13/deployments/${encodeURIComponent(process.env.VITE_VERCEL_URL)}`, {
headers: {
Authorization: `Bearer ${process.env.VERCEL_TOKEN}`,
},
})
// Return type definitions
// https://github.com/vercel/vercel/blob/main/packages/client/src/types.ts#L44C1-L95C2

However, upon inspection of the response I noticed that there were additional build env variables that piqued my interest.

Shortened Response
{
"id": "dpl_7HKRbiAseMhnz4y6jMW3ykjXP3cy",
"build": {
"env": [
"CI",
"VERCEL",
"VERCEL_ENV",
"TURBO_REMOTE_ONLY",
"TURBO_RUN_SUMMARY",
"NX_DAEMON",
"VERCEL_URL",
"VERCEL_GIT_PROVIDER",
"VERCEL_GIT_PREVIOUS_SHA",
"VERCEL_GIT_REPO_SLUG",
"VERCEL_GIT_REPO_OWNER",
"VERCEL_GIT_REPO_ID",
"VERCEL_GIT_COMMIT_REF",
"VERCEL_GIT_COMMIT_SHA",
"VERCEL_GIT_COMMIT_MESSAGE",
"VERCEL_GIT_COMMIT_AUTHOR_LOGIN",
"VERCEL_GIT_COMMIT_AUTHOR_NAME",
"VERCEL_GIT_PULL_REQUEST_ID",
"VERCEL_EDGE_CROSS_REQUEST_FETCH",
"VERCEL_ALLOW_AWS_ENV_VARS",
"ENABLE_VC_BUILD",
"VERCEL_BUILD_OUTPUTS_EDGE_FUNCTION",
"VERCEL_EDGE_FUNCTIONS_REGIONAL_INVOCATION",
"VERCEL_EDGE_FUNCTIONS_EMBEDDED_SOURCEMAPS",
"VERCEL_EDGE_FUNCTIONS_STRICT_MODE",
"USE_OUTPUT_FOR_EDGE_FUNCTIONS",
"NEXT_PRIVATE_MULTI_PAYLOAD",
"VERCEL_RICHER_DEPLOYMENT_OUTPUTS",
"VERCEL_EDGE_SUSPENSE_CACHE",
"VERCEL_SERVERLESS_SUSPENSE_CACHE",
"VERCEL_BUILD_MONOREPO_SUPPORT",
"VERCEL_USE_NODE_BRIDGE_PRIVATE_LATEST",
"VERCEL_ENABLE_NODE_COMPATIBILITY",
"VERCEL_ARTIFACTS_JWT_AUTH",
"VERCEL_DEPLOYMENT_SKEW_HANDLING",
"VERCEL_ENCRYPT_DEPLOYMENT_BUILD_ENV"
]
},
"env": [
// These are available at runtime
"VERCEL",
"VERCEL_ENV",
"TURBO_REMOTE_ONLY",
"TURBO_RUN_SUMMARY",
"NX_DAEMON",
"VERCEL_URL",
"VERCEL_GIT_PROVIDER",
"VERCEL_GIT_PREVIOUS_SHA",
"VERCEL_GIT_REPO_SLUG",
"VERCEL_GIT_REPO_OWNER",
"VERCEL_GIT_REPO_ID",
"VERCEL_GIT_COMMIT_REF",
"VERCEL_GIT_COMMIT_SHA",
"VERCEL_GIT_COMMIT_MESSAGE",
"VERCEL_GIT_COMMIT_AUTHOR_LOGIN",
"VERCEL_GIT_COMMIT_AUTHOR_NAME",
"VERCEL_GIT_PULL_REQUEST_ID"
]
}
Modified built script to print env variables
"scripts": {
"build": "printenv && solid-start build",
},
// package.json
"scripts": {
"build": "VITE_VERCEL_DEPLOYMENT_ID=$VERCEL_DEPLOYMENT_ID solid-start build",
},
// or vite.config.ts
export default defineConfig(() => {
define: {
"import.meta.env.VITE_VERCEL_DEPLOYMENT_ID": JSON.stringify(process.env.VERCEL_DEPLOYMENT_ID),
},
})

The Solid glue

All we need to do now is add the deloyment id to our asset requests. Being unfamiliar with how js assets are loaded, I looked at the Solid build output. We can see the dynamic imports call an import function and the browser requests the file. Although there are other ways assets are requested I’ll only focus on this.

.vercel/output/static/assets/index-[hash].js
const x = m("<main><button>load dynamic data (<!#><!/>)");
function u() {
return (() => {
...
return (
a.nextSibling,
(t.$$click = () => {
d(() => import("./data-512a1e3e.js"),
["assets/data-512a1e3e.js", "assets/data-cd2f470e.css"])
.then((r) => {
console.log("loaded data", r.data);
});
}),
...
);
})();
}

I settled on a quick and dirty solution of creating a Vite plugin that adds the deployment search param to dynamic imports.

What the plugin does

Map VERCEL_DEPLOYMENT_ID to import.meta.env.VITE_VERCEL_DEPLOYMENT_ID so we no longer need the modified build script to reassign the variabl

When building client bundle add __deploymentImport() to the client entry, entry-client.tsx Replace dynamic imports to use the dynamic import function instead of the usual import("file.js")

vite.config.ts
const vercelDeploymentImports = (): Plugin => {
const deploymentId = process.env.VERCEL_DEPLOYMENT_ID
let pluginEnabled = false
let clientEntry: string | undefined
return {
name: "vercel-skew-handling",
config(config: any, env) {
pluginEnabled =
env.command === "build" &&
env.mode === "production" &&
env.ssrBuild === false &&
!!deploymentId &&
deploymentId !== ""
clientEntry = config.solidOptions.clientEntry
return {
define: {
"import.meta.env.VITE_VERCEL_DEPLOYMENT_ID": JSON.stringify(deploymentId ?? ""),
},
}
},
transform(code, id) {
if (!pluginEnabled || !clientEntry || id !== clientEntry) {
return
}
console.log("Adding deploymentImport to client entry", id)
return {
code:
`
window.__deploymentImport = (file) => {
const url = new URL(file, import.meta.url)
url.searchParams.set('dpl', import.meta.env.VITE_VERCEL_DEPLOYMENT_ID)
return import(url)
};
` + code,
}
},
renderDynamicImport(options) {
if (!pluginEnabled || !options.targetModuleId) {
return
}
return {
left: "window.__deploymentImport(",
right: ")",
}
},
}
}
export default defineConfig({
plugins: [
solid({
adapter: vercel(),
}),
vercelDeploymentImports(),
],
})

Let’s build and check the output again. We should see dynamic imports use our custom function and its declaration in the client entry. This can also be verified by inspecting the network tab in the browser to see our assets have the dpl search param.

.vercel/output/static/assets/entry-client-[hash].js
...
window.__deploymentImport=e=>{
const t=new URL(e,import.meta.url);
return t.searchParams.set("dpl","dpl_7q58fx9MpVN74M7nz9tUfgQALVve"),Qe(()=>import(t),[])
}
// .vercel/output/static/assets/index-[hash].js
const x = m(
'<main class="text-center mx-auto text-gray-700 p-4"><button>load dynamic data (<!#><!/>)'
);
function $() {
return (() => {
...
return (
a.nextSibling,
(t.$$click = () => {
s(() => window.__deploymentImport("./data-e597730f.js"), void 0)
.then(
(o) => {
console.log("loaded data", o.data);
}
);
}),
...
);
})();
}

While I was focused on getting this to work with SolidStart, I believe this could work with any framework deployed to Vercel!

Similar solution that requests based off deployment ids could be implemented on other serverless platforms.