Skip to content

Services

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

A WebsiteService is one provisioned integration attached to a Website. The GitHub repo is a service. The Vercel project is a service. A Shopify store, an OpenRouter key, a Resend domain, a TikTok channel — each is a separate service row.

type ServiceType =
| "github" | "vercel" | "openrouter" | "resend"
| "bookings" | "seo" | "commerce"
| "channel_tiktok" | "channel_facebook" | "channel_pinterest"
| "channel_google" | "channel_marketplace_connect";
interface WebsiteService {
websiteId: string;
serviceType: ServiceType;
externalId: string; // the integration's id for this resource
externalUrl?: string; // a link to view it in the vendor's dashboard
secretArn?: string; // pointer to Secrets Manager
metadata?: Record<string, unknown>;
createdAt: string;
}

(websiteId, serviceType) is the primary key — at most one row per type per Website. Channels are an exception via the typed variants (channel_tiktok, channel_facebook, …); each is its own type.

commerce and channel_* are platform-agnostic. The actual backend is recorded on metadata.platform:

{
websiteId: "wb_01J7...",
serviceType: "commerce",
externalId: "acme.myshopify.com",
metadata: { platform: "shopify" }
}

Today every commerce row has platform: "shopify", but the type deliberately doesn’t bake that in — if a future adapter ships (BigCommerce, Medusa), existing data and the surrounding type system don’t need to change.

ServiceWhat externalId holdsWhat’s in metadata
githubrepo full name (org/client-slug)default branch, last commit
vercelproject idproduction deployment id, deploy url
openrouterkey idusage stats snapshot
resenddomain idDNS verification status
bookingscal.com usernamebrand color, public URL
seotier namecapability snapshot, scan results, GEO probes
commerceplatform’s canonical store idplatform, currency, scopes granted
channel_*channel handle ("tiktok", etc.)platform, install status, publication id

secretArn points to Secrets Manager. Access tokens (Shopify Admin API tokens, etc.) live there, not in the WebsiteService row. Reasons: rotation, audit, fine-grained IAM, and not putting credentials in a row that gets dumped in console logs.

To use a token, fetch it from Secrets Manager via the secretArn. The integrations layer (packages/api/src/integrations/*.ts) handles this for you.

Terminal window
# CLI doesn't currently surface this; use SDK:
const { services } = await vc.websites.services("wb_01J7...");
for (const s of services) {
console.log(s.serviceType, s.externalUrl);
}

The REST endpoint is GET /v1/websites/:id/services.

Recent activity per service (deployments, AI calls, scan runs) lives on a sibling endpoint:

const { logs } = await vc.websites.logs("wb_01J7...", "vercel");

Each integration emits its own log shape — see the REST API reference for the exact union.

You don’t typically create service rows directly. Two patterns:

  1. Provisioning the Website creates the always-on servicesgithub + vercel are always created during initial provisioning, never separately.
  2. Toggling features creates optional servicesvantage websites features <id> --openrouter provisions the OpenRouter key + creates the service row in one step.

The exceptions are OAuth-based services (Shopify, channels) which are created when the merchant completes an OAuth flow through the portal, not by an API call.

Service teardown is per-feature and happens automatically when:

  • The Website is deleted (every service torn down)
  • A feature flag flips false (only that service torn down)
  • A Shopify app is uninstalled (commerce + dependent channel_* rows removed via webhook)