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.
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
createsordeletesonly with an SDK-created resource ref. - One operation cannot contain both
createsanddeletes, 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 andNaNvalues are rejected; zero is a valid authored maximum.- A resource cap belongs in plan
limits, not plangrants. 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.