Farther ShoreDocs
Go to Farther Shore
The fs.business() program
Meters & measures
Counted resources
ScopeCount sourceEnforcement behaviorCorrectness rulesInspect the compiled effect
Routes & access groups
Route groups & grants
Plans & pricing
The build output
Team RBAC
Tenancy & identity
Frontend integrations
@farthershore/business
Add metered routes
Add a resource limit
Add team RBAC
@farthershore/business exports
@farthershore/business/codegen exports
Status
Docs/Build your product/Counted resources

Counted resources

Bound persistent inventory such as projects, seats, or workspaces.

A resource counts things a subscriber owns. Unlike usage, the count can go down when an object is deleted.

ts
import * as fs from "@farthershore/business";
fs.backend("api");
const requests = fs.requests();

const projects = fs.resource("projects", {
  cap: fs.scope.subscription,
  countSource: "action_inferred",
});

const createProject = fs.route("/v1/projects", {
  post: { creates: projects },
});
const deleteProject = fs.route("/v1/projects/{id}", {
  delete: { deletes: projects },
});

fs.plan("starter", {
  kind: fs.plan.kind.flat,
  price: fs.money.usd(9).monthly(),
  grants: [createProject, deleteProject],
  limits: [requests.perMinute(600), projects.max(3)],
});

export default fs.business();

Scope

Use fs.scope.subscription for inventory owned by the subscribing customer. Use fs.scope.subject only when each verified principal has an independent count. Pick the same owner that your backend uses for the underlying rows. Subscription is the default scope. Subject scope requires a subjectType; subscription scope rejects subjectType. cap selects the scope, not the numeric maximum; the plan's projects.max(3) sets that maximum. If both cap and scope are supplied, cap wins; prefer one spelling.

Count source

Set countSource: "action_inferred" when using creates and deletes. The default is "reported", not automatic route inference. These fields let the platform infer changes from successful route operations. This is suitable only when one successful call has one unambiguous effect.

For batch, asynchronous, imported, or out-of-band changes, report the authoritative count through the resource-count operation instead of pretending the route effect is exact. Use countSource: "reported" for that model; an absolute count replaces the stored count, rather than adding a delta.

Enforcement behavior

The gateway checks a create mutation before calling the backend. A subscriber at its cap is denied. Deletes remain callable so the customer can return below the cap.

Lowering a limit does not delete customer data. Existing customers may become over-limit and cannot create more until their count falls or their plan changes. Treat a lower resource cap as a restrictive plan change and inspect its subscriber impact before publish.

For action-inferred creates, Core reserves one count before forwarding, using the subscription's frozen compiled plan and environment. A 2xx result commits that reservation; a non-2xx result releases it. Successful deletes decrement the count, floored at zero. Repeated finalization of the same platform mutation is idempotent; this does not make separate client requests idempotent in your application. A 202 response is also 2xx, so do not model an asynchronous job as a completed inventory creation unless that is genuinely its meaning.

The count is authoritative in Core, not an edge-local counter. Reservation and cap checking share a guarded database write; do not implement a separate read-count-then-create check and assume it has the same concurrency guarantee. Counts are keyed by business, environment, subscription, resource and optional subject. Platform reservation does not atomically commit your external database; keep reconciliation and application idempotency in your integration tests.

Correctness rules

  • Use creates or deletes only with an SDK-created resource ref.
  • One operation cannot contain both creates and deletes, even if they name different resources. These fields describe one resource effect, not a batch or a transfer between inventories.
  • resource.max(n) accepts non-negative integers only. Negative, fractional, infinite and NaN values are rejected; zero is a valid authored maximum.
  • A resource cap belongs in plan limits, not plan grants. It does not replace the complete request-rate limit required by the build, as the example shows.
  • Keep the reported count and backend source of truth consistent.
  • Make create handlers idempotent because client and gateway retries can repeat a request.

Inspect the compiled effect

The SDK lowers creates/deletes to an action with the resource ID and a create/delete effect, then links the route operation to that action. If you omit an explicit action ID, it derives one from the HTTP method and path. Inspect both the route and action in the built IR; merely declaring a resource does not attach a mutation to an endpoint. References must come from the same SDK registry generation and still resolve to a declared resource.

PreviousMeters & measuresNextRoutes & access groups

On this page

ScopeCount sourceEnforcement behaviorCorrectness rulesInspect the compiled effect