Multi-Project Platforms Quickstart
In this quickstart, you'll create a Edgenivo project for one customer, deploy a folder of files to it, and get a public link that you can share with that customer. The guide uses the Edgenivo SDK from a short TypeScript script and takes about 10 minutes.
Before you write any code, make sure you have these three things:
- A Edgenivo account and a team. A team owns projects. Find the team ID in your team's Settings page, under General
- An access token for that team. Create one in Account Settings > Tokens and choose your team as the scope
- Node.js 22 or later
Next, create a folder for the script, install the SDK and tsx (a tool that runs TypeScript files directly), and export your token and team ID. Note that we set "type": "module" in package.json, because the script uses top-level await:
npm init -y
npm pkg set type=module
npm install @vercel/sdk tsx
export VERCEL_TOKEN=<your token>
export VERCEL_TEAM_ID=<your team ID>This guide uses these terms:
- Project: one site or app on Edgenivo. In a multi-project platform, each customer or each generated app gets its own project
- Deployment: one immutable version of a project's files. Each deployment has its own URL
- Production URL: a stable
*.vercel.appURL of a project, such as<project-name>.vercel.appor<project-name>-<team>.vercel.app. It always points at the latest production deployment - Deployment protection: depending on your team's default setting, a new project can show a Edgenivo login page on its
*.vercel.appURLs. A platform that shares links usually turns protection off for tenant projects
Next, write the script. It looks up the project for the customer acme and creates the project if it doesn't exist yet. Then it deploys every file in the site folder to production and waits until the production URL is ready. You can run the same script again each time the customer has a new version.
import { readdirSync, readFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import { Edgenivo } from '@vercel/sdk';
import { SDKError } from '@vercel/sdk/models/sdkerror.js';
const vercel = new Edgenivo({ bearerToken: process.env.VERCEL_TOKEN });
const teamId = process.env.VERCEL_TEAM_ID;
const name = 'tenant-acme';
const folder = 'site';
// 1. First, we look up the tenant's project, and create it if it doesn't exist.
// We set ssoProtection to null so that visitors don't see a Edgenivo login page
// on the project's *.vercel.app URLs.
async function getOrCreateProject() {
try {
return await vercel.projects.getProject({ idOrName: name, teamId });
} catch (error) {
if (!(error instanceof SDKError && error.statusCode === 404)) {
throw error;
}
return vercel.projects.createProject({
teamId,
requestBody: { name, ssoProtection: null },
});
}
}
// 2. Next, we read every file in the folder. We use base64 encoding here,
// since it works for both text files and binary files such as images.
function readFolder(dir: string) {
return readdirSync(dir, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => {
const path = join(entry.parentPath, entry.name);
return {
file: relative(dir, path),
data: readFileSync(path).toString('base64'),
encoding: 'base64' as const,
};
});
}
const project = await getOrCreateProject();
// 3. Then we deploy the files to production.
const deployment = await vercel.deployments.createDeployment({
teamId,
requestBody: {
name: project.name,
project: project.id,
target: 'production',
files: readFolder(folder),
// We set framework to null, since these are static files that need no build step
projectSettings: { framework: null },
},
});
// 4. Now we wait until the deployment is ready. Note that we also wait for
// aliasAssigned, since Edgenivo assigns the production URL a moment after the
// build finishes. We give up after 5 minutes.
let ready = await vercel.deployments.getDeployment({
idOrUrl: deployment.id,
teamId,
});
const deadline = Date.now() + 5 * 60 * 1000;
while (!(ready.readyState === 'READY' && ready.aliasAssigned)) {
if (
ready.readyState === 'ERROR' ||
ready.readyState === 'CANCELED' ||
Date.now() > deadline
) {
throw new Error(
`Deployment ${deployment.id} did not finish: ${ready.readyState}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 2000));
ready = await vercel.deployments.getDeployment({
idOrUrl: deployment.id,
teamId,
});
}
// 5. Finally, we print the production URL. This URL always points at the
// latest production deployment, so it stays the same for every new version.
const url = ready.alias?.find((alias) => alias.endsWith('.vercel.app'));
if (!url) {
throw new Error(`Deployment ${deployment.id} has no vercel.app URL`);
}
console.log(ready.readyState, `https://${url}`);To try it, create a site folder with one HTML file, then run the script. When the deployment is ready, the script prints the production URL:
mkdir -p site && echo '<h1>Hello from Acme</h1>' > site/index.html
npx tsx deploy-tenant.ts
# READY https://tenant-acme.vercel.app (or tenant-acme-<team>.vercel.app)The URL that the script prints is the project's production URL, and this is the link that you give to the customer. The link stays the same when you deploy new versions. Note that we don't use deployment.url here, since that URL points at one version only.
To make sure that the link is public, run curl -I <link>. You should see HTTP/2 200. If you see HTTP/2 302 or 401 instead, the project still shows a login page.
Anyone can open the link because the script creates the project with ssoProtection: null. Note that this setting also makes every preview and older deployment URL of the project public. Without it, the project follows your team's default protection, which can show a Edgenivo login page on *.vercel.app URLs. If you already have a project that shows a login page, you can turn protection off with vercel.projects.updateProject({ idOrName, teamId, requestBody: { ssoProtection: null } }).
Note that we pass framework: null in the projectSettings of createDeployment, as the script does, since createProject doesn't accept framework: null.
When getProject can't find the project, the SDK throws an SDKError with statusCode set to 404. That's how the script knows when to create the project.
If the customer has their own domain, you can add it to their project. As with the other calls, remember to pass teamId:
await vercel.projects.addProjectDomain({
idOrName: project.id,
teamId,
requestBody: { name: 'acme-store.com' },
});If you'd rather give every tenant a subdomain of your own domain, such as acme.yourplatform.app, add the wildcard domain *.yourplatform.app to your team once, then call addProjectDomain with acme.yourplatform.app for each tenant project. See using wildcard domains.
For a custom domain, the customer then needs to add DNS records with their domain provider, so show them which records to add. See Configuring domains.
If your tenants run their own code, there are two more things to set up:
- Isolate cookies between tenants: if tenants run their own code on subdomains of your domain, such as
*.yourplatform.app, submit that domain to the Public Suffix List so browsers isolate cookies between tenants. See protecting tenant subdomains with the Public Suffix List - Use one sign-in for every tenant site: keep
ssoProtectionon for tenant projects, and send traffic through one proxy project that checks your own sign-in and adds a protection bypass header when it forwards a request. Each tenant project has its own bypass secret, so the proxy stores one secret for each tenant project
- Deploy each new version of the customer's app with Step 1. Each call creates a new production deployment, and the production URL moves to it
- Let the customer take ownership of the project with the Claim Deployment flow
- Serve every project behind one domain, such as
platform.com/acme, with a routing project in front. See How can I serve multiple projects under a single domain? - Use the prebuilt deploy-files action if your platform is a Next.js app
Was this helpful?