Skip to content

Provisioning

import { Aside } from ‘@astrojs/starlight/components’;

Provisioning is the multi-step pipeline that turns vantage websites create into an actually deployed site. It runs asynchronously on a worker Lambda, persists per-step progress to DynamoDB, and retries on failure.

Creating a Website touches GitHub, Vercel, and (optionally) several other vendors. End-to-end it takes 60-90 seconds for a basic site, longer with features. That doesn’t fit in an API request.

Splitting the work into a SQS-queued job has two benefits:

  1. The API responds immediately — callers get a websiteId they can poll
  2. The job is idempotent per-step and resumable on failure — if vercel_create_project fails because Vercel was 503, the job retries that step without redoing the GitHub steps
github_create_repo
github_invite_collaborator
vercel_create_project
openrouter_create_key (only if features.openrouter)
resend_create_domain (only if features.resend)
vercel_set_env_vars
vercel_initial_deploy
finalize (flips Website.status to "active")

Each step is one function in packages/api/src/provisioning/orchestrator.ts.

Every job is one row in vantage-provisioning-jobs:

type ProvisioningStep =
| "github_create_repo"
| "github_invite_collaborator"
| "vercel_create_project"
| "openrouter_create_key"
| "resend_create_domain"
| "vercel_set_env_vars"
| "vercel_initial_deploy"
| "finalize";
interface ProvisioningJob {
jobId: string;
websiteId: string;
status: "queued" | "running" | "succeeded" | "failed";
/** The step the worker is currently inside (omitted once the job ends). */
currentStep?: ProvisioningStep;
/** Steps already finished, in the order they completed. */
completedSteps: ProvisioningStep[];
/** Per-step failures — usually empty; populated when a step throws. */
errors: Array<{ step: ProvisioningStep; message: string; at: string }>;
startedAt: string;
completedAt?: string;
}

The portal’s Blueprint canvas polls this so each node turns green as the corresponding step name lands in completedSteps — gives you the visual progress indicator.

const { jobs } = await vc.websites.jobs("wb_01J7...");
for (const j of jobs) {
console.log(j.status, "currently:", j.currentStep, "done:", j.completedSteps);
}

Or via REST: GET /v1/websites/:id/jobs.

Step fails transiently (vendor 5xx, network blip): The worker retries the step up to N times with exponential backoff. Job stays running.

Step fails permanently (4xx, validation error): A { step, message, at } entry is appended to errors, the job’s status flips to failed, and the Website’s status flips to failed too. You can delete the Website and start fresh, or fix the underlying issue and re-trigger (today: by deleting + recreating; future: a retry endpoint).

Worker crashes mid-step: SQS visibility timeout expires, message redelivered, worker picks up where it left off. Steps already in completedSteps are skipped. This is why steps must be idempotent — creating a GitHub repo that already exists should not be an error.

When you call vantage websites features <id> --openrouter:

  1. API updates the features flag on the Website row
  2. API enqueues a feature-delta job
  3. Worker compares before/after, runs only the steps for changed features:
    • openrouter: false → trueopenrouter_create_key
    • resend: true → falseresend_teardown_domain

The full pipeline is only run on create. Feature toggles are mini-pipelines.

Deleting a Website enqueues a teardown job that runs the per-service teardown handlers in packages/api/src/provisioning/teardown.ts. Same observability, same retry semantics, same per-step state in the jobs table.