Sidecars
Describe your product with two YAML files
A sidecar is two files at the root of your repo. They travel with every deploy and are the source of truth for what the catalog says about your product — name, links, imagery, features and pricing.
What a sidecar is
| File | Holds |
|---|---|
blueforge.product.yml | Identity: id, name, tagline, status, links, assets, locales |
blueforge.features.yml | Marketing message: feature bullets, categories, tags, pricing, promotions |
On a platform deploy, forge-control reads both files, validates them with @blueforge-studio/blueforge-sidecar-schemas and sends them with the deployment registration. From your own CI you send the same content yourself in the product and features fields of POST /api/deployments. The site merges the two into one record per product.
The split is deliberate: identity changes rarely and is reviewed; copy can change on any deploy without touching the identity file.
Minimal example
# blueforge.product.yml — identity
version: "2.0.0"
products:
- id: acme-store
name: Acme Store
apps: [site]
primaryApp: site
status: Live
tagline: Inventory and orders in one place
description: >-
Acme Store keeps stock, orders and fulfilment in sync across every
channel you sell on.
ecosystems: [business]
links:
site: https://acme.example.com
others:
- label: Documentation
href: https://acme.example.com/docs
assets:
logo: public/brand/logo-512.webp
og: public/brand/og-1200x630.webp
cover: public/brand/cover.webp
screenshots:
- path: public/screens/orders.webp
caption: Orders from intake to delivery
alt: The orders dashboard
i18n:
locales: [en, es]# blueforge.features.yml — marketing message
version: "2.0.0"
features:
- product: acme-store
features:
- Stock levels synced across channels
- One order queue for every storefront
categories:
- slug: e-commerce
label: E-commerce
tags: [inventory, orders]
pricing:
model: freemium
currency: USD
tiers:
- name: Free
price: "$0"
- name: Pro
price: "$19/mo"
recommended: true
promotions:
- code: LAUNCH20
label: 20% off your first three months
valid_until: "2026-12-31"Product fields
The file is { version: "2.0.0", products: [...] } with at least one entry. Every object is strict: an unknown or misspelled key is a validation error, not a silently ignored field.
| Field | Rules |
|---|---|
id | Required. The product's catalog key — see Identity below. |
name | Required. Display name. |
apps / primaryApp | At least one is required. The app names (as in blueforge.config.json) that belong to this product; primaryApp is the one whose URL the catalog links. |
status | One of Live, Beta, In Development, Coming Soon, Planned. |
tagline, description, longDescription, summary | Copy. summary is one short sentence used on cards. |
ecosystems | List of ecosystem ids the product appears under. |
icon, color, gradient | Card styling hints (icon name and Tailwind classes). |
links | site, app, and others — a list of { label, href } where href must be a URL. |
assets | logo, og, cover (repo-relative paths) and screenshots — a list of { path, caption?, alt? }. |
i18n.locales | The languages the product ships (BCP-47 tags, at least one). |
collection | Optional kebab-case id grouping related products into one family. |
Features fields
The file is { version: "2.0.0", features: [...], promotions?: [...] }. Each features[] row names the product it describes with product:, which must match an id in the product file.
| Field | Rules |
|---|---|
product | Required. The product id this row describes. |
features | Feature bullets. A plain string, or an object with text plus an image and its alt text. |
categories | List of { slug, label? }; slug is kebab-case. |
tags | List of strings. |
pricing | model (free, freemium, subscription, one-time, contact, enterprise), optional ISO-4217 currency, and at least one tier { name, price, description?, recommended?, includes?, cta? }. At most one tier may be recommended. pricing: null hides pricing deliberately. |
promotions | Top level, up to 20: { code, label, url?, valid_from?, valid_until?, terms? }; dates are YYYY-MM-DD and valid_from may not be after valid_until. |
Identity and precedence
- The
idis the load-bearing key. Changing it creates a second catalog card; the old id stays until its registration is retired. Pick it once. - The sidecar wins. Once a deploy has registered a sidecar for a product, its copy overrides any other description of the same id the catalog holds.
- First writer owns the app. A registration is keyed by
(repo, app)and belongs to the organization that registered it first; another organization writing the same pair is refused with403 tenant-mismatch.
Assets
Asset paths are repo-relative. The platform hashes each declared file and uploads only the ones whose sha256 changed; the site renders every size it needs from the upload. Any format the image pipeline decodes is accepted (png, jpeg, webp, gif, avif, svg, tiff).
| Kind | Used for |
|---|---|
logo | Cards, product page, search — square, transparent preferred |
og | og:image / twitter:image on the product page (1200×630) |
cover | Product page hero and listing cards; the social card when there is no og |
screenshots | The product page gallery, in the order declared |
The upload protocol, the fallbacks and the approval gate are on Asset sync.
Versioning
Both files carry version: "2.0.0", the current schema version. The registration endpoint also accepts the older single-file shape (one product's fields at the top level of blueforge.product.yml), so an older repo keeps registering while it migrates — but new repos should write the two-file form above.
Imagery is versioned by content: a replaced file is a new object at a new URL, so caches never serve a stale image under a fresh name.
Schema changes
The schemas are strict, so a new field is only accepted once your pinned version of the schemas package knows it. Bump the package in the same change that starts using a new field.
Validate in CI
Parse the YAML and run it through the schema before you deploy; a failure lists every issue with its path.
import { readFileSync } from "node:fs";
import { parse } from "yaml";
import { parseProductSidecar, parseFeaturesSidecar } from "@blueforge-studio/blueforge-sidecar-schemas";
const product = parseProductSidecar(parse(readFileSync("blueforge.product.yml", "utf8")));
const features = parseFeaturesSidecar(parse(readFileSync("blueforge.features.yml", "utf8")));
for (const r of [product, features]) {
if (!r.ok) {
console.error(r.issues);
process.exit(1);
}
}yaml is used here as an example.