Farther ShoreDocs
Go to Farther Shore
What is FartherShore
Install the CLI
Quickstart
Core concepts
The @Product class
Meters & resources
Features & routes
Capabilities & entitlements
Plans & pricing
The Manifest IR
The envelopeDeterminism — the gateValidationErrors you'll hitBuild and apply
Bring your own backend
Transport modes
Metering & verification
Runtime tokens
Frontend SDK
Root & data components
Auth & sessions
Entitlement gates
Connect Stripe
Subscriptions & usage
Plan changes & grandfathering
Billing strategies
Apply & deploy
Environments
Migrations
Docs versions & archive
Operate with an agent
Operation classes
MCP server
End-to-end via CLI/MCP
CLI reference
@farthershore/product
@farthershore/backend
@farthershore/farthershore-js
Environment variables
Response & deny codes
Add a metered capability
Gate a feature
Change a price
Prepaid credits
Meter AI tokens
Operate via an agent
Prepare for launch
Status
Docs/Define your product/The Manifest IR

The Manifest IR

How the decorated @Product class compiles to deterministic, validated intermediate representation.

PreviousPlans & pricingNextBring your own backend

The Manifest IR is what your @Product class becomes. The SDK runs the decorators, assembles a single JSON envelope, validates it against the platform schema, and hashes it. Core accepts the IR — not your TypeScript — so the IR is the contract boundary. It is deterministic by construction: the same product always produces byte-identical bytes and the same hash.

The envelope

compileProductToManifest(product) returns { ir, irHash }. The ir is a ManifestIrDocument:

type ManifestIrDocument = {
  irVersion: 1;
  sdkVersion: string;
  product: ProductSpecJson;     // product block, metering, plans, surfaces, frontend, resources, …
  routes: RouteLayerJson[];     // one layer per @Feature, routes in declaration order
  policies: PolicyLayerJson[];
  capabilities: CapabilityLayerJson[];
};

product.product holds the product block (name, baseUrl = your origin, displayName); product.metering.meters holds the compiled meters; product.plans holds the sorted plans; routes holds one layer per feature. Runtime state (rollout, flags, migrations) is not part of the IR — Manifest IR v1 is a pure snapshot, and a top-level runtime key is rejected by validation.

Determinism — the gate

product/product.config.ts and everything it imports must be deterministic: no Date.now(), no randomness, no network, no process state. Collections that don't carry semantic order (plans, meters, capabilities) are sorted by key, so declaration order doesn't change the bytes; routes keep declaration order because the gateway matcher is first-match-wins.

The GitHub bot builds the IR twice and rejects the push if the two hashes differ. So a reordered-but-equivalent class hashes identically:

// Declaring @Plan("pro") before @Plan("starter"), or reversing meters, yields the SAME irHash —
// sorted collections are order-insensitive; only route order is preserved.
expect(compile(cronCloud()).irHash).toBe(compile(reorderedCronCloud()).irHash);

The IR hash is a 64-hex SHA-256 over the canonical JSON:

import { hashIr, canonicalIrJson } from "@farthershore/product";

const json = canonicalIrJson(ir); // stable, sorted, whitespace-normalized JSON string
const hash = hashIr(ir);          // matches /^[0-9a-f]{64}$/

canonicalIrJson is the exact serialization the hash is taken over — useful for diffing two builds or proving determinism in CI.

Validation

validateManifestIr(candidate) checks a candidate envelope against the bundled platform schema and returns a discriminated result:

import { validateManifestIr } from "@farthershore/product";

const result = validateManifestIr(candidate);
if (result.ok) {
  result.ir;      // the JSON-normalized document (author's bytes, undefined keys dropped)
  result.irHash;  // its hash
} else {
  result.issues;  // [{ code, path, message }, …] — structured, path-addressed
}

Schema violations come back as structured issues with a dotted path, so you can pinpoint the offending field:

@Product({ name: "bad", origin: "not-a-url", primaryColor: "red" })
// issues include paths: "product.product.baseUrl", "product.product.primaryColor"

On success, result.ir is the JSON-normalized original candidate, not a schema-transformed value: validation proves the document is acceptable, but the bytes stay yours.

Two validation passes

compileProductToManifest runs the structural schema validation (throwing a ManifestValidationError with .issues on failure) and graph checks (missing references, duplicate keys, route-metering conflicts throw ManifestBuilderError). A second, completeness pass runs at build time:

import { validatePlanRateLimitCompleteness } from "@farthershore/product";

const issues = validatePlanRateLimitCompleteness(ir);
// → [{ code: "PLAN_RATE_LIMIT_REQUIRED", path: "product.plans.0.limits", message: … }]
//   for every plan missing a rate-limit rule. Empty array = complete.

This is run by farthershore-manifest-build after a clean compile, and uses the same predicate as the publish gate — so a plan that passes locally passes at publish. It is deliberately kept out of compileProductToManifest so the byte-identity fixtures can compile intentionally-limitless products.

Errors you'll hit

ErrorThrown when
ManifestBuilderErrorDecoration/graph problems: missing origin, bad route key, duplicate plan/action id, undeclared meter reference, both-cost-and-report conflict, integer-like dimension key.
ManifestValidationErrorThe assembled IR violates the platform schema (.issues carries the dotted paths).

Both are exported from @farthershore/product. Builder errors fire as early as possible — most at decoration time, the moment the class is defined — so a malformed product fails before it ever reaches the platform compiler.

Build and apply

# Compile product/product.config.ts → validated, hashed Manifest IR
farthershore-manifest-build --entry product/product.config.ts --out manifest-ir.json

This is the same binary the GitHub bot, build-runner, and CLI share. A clean local build is the same IR the platform will accept. To apply: commit product/** and push — the bot validates, builds twice for determinism, and publishes the accepted release through Core so edge artifacts propagate.

The IR carries sdkVersion and irVersion: 1. Pin @farthershore/product together with @farthershore/backend and @farthershore/farthershore-js at the same version — they release in lockstep, and the SDK projects the platform contract at build time.

Start from the @Product class and work outward through meters, features, capabilities, and plans — every one of them is just more bytes in this envelope.