Back to blog

Vercel Skew Protection with SolidStart

  • frontend
  • solid.js
  • solidstart
  • vercel

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 to asset requests was sufficient, indicating that this is a platform feature.

Trying it out

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

Page with dynamic import
tsx
// 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. To get the deployment id I viewed the deployment on the Vercel dashboard.

bash
# 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 to test this theory would be the start.


Other findings

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

bash
curl '.../assets/data-1b6755f5.js?dpl=dpl_GntXEvUxWfFv3SWK3XYjE7EtPf6f'
  • When both deployment search param and header is set, search param is used.
  • Deployment id dpl_ prefix is optional.

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.

tsx
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
json
{
"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
json
"scripts": {
"build": "printenv && solid-start build",
},
  • VERCEL_DEPLOYMENT_SKEW_HANDLING was set to 1 . I could not find any info on what it does. I suspect this enables the skew protection route handling.
  • VERCEL_DEPLOYMENT_ID is the equivalent to Nextjs’s NEXT_DEPLOYMENT_ID, however this variable is not exposed during runtime. Since Vite replaces import.meta.env variables at build time we can use this, but we have to prefix it with VITE_ (Vite docs). We now have our deployment id.
    json
    "scripts": {
    "build": "VITE_VERCEL_DEPLOYMENT_ID=$VERCEL_DEPLOYMENT_ID solid-start build",
    },

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
tsx
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 variable.
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
tsx
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.

tsx
// .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 could be implemented on other serverless platforms maybe requiring additional route handling.