Gate API routes by plan
Grant route refs to selected plans and verify denial at the edge.
Use direct route grants when only selected plans should reach an operation. The gateway, not the UI, is the authority.
Outcome
Each plan receives exactly the API operations it grants by ref.
Prerequisites
- Existing plans and a preview environment
- Stable route paths or explicit action ids
- Test subscribers on both an allowed and denied plan
import * as fs from "@farthershore/business";
const requests = fs.requests();
const api = fs.backend("api", {
transport: { mode: "direct" },
meters: [requests],
default: true,
});
const listJobs = fs.route("/v1/jobs", {
get: { backend: api, costs: [requests.fixed(1)] },
});
const createJob = fs.route("/v1/jobs/create", {
post: { backend: api, costs: [requests.fixed(1)] },
});
const deleteJob = fs.route("/v1/jobs/{id}", {
delete: { backend: api, costs: [requests.fixed(1)] },
});
const managedJobs = fs.group("managed-jobs", [listJobs, createJob, deleteJob]);
fs.plan("starter", {
kind: fs.plan.kind.free,
grants: [listJobs],
limits: [requests.perMinute(60)],
});
fs.plan("pro", {
kind: fs.plan.kind.flat,
price: fs.money.usd(29).monthly(),
grants: [managedJobs],
limits: [requests.perMinute(600)],
});
export default fs.business();
Starter can list jobs but cannot mutate them. Pro grants the group and can use all three operations.
Verify in preview
After the push, wait for route-grants-preview to appear in env list before
creating the backend row. If automatic branch-prefix creation did not occur,
create the preview explicitly first:
farthershore build --format json
farthershore validate --format json
git push -u origin HEAD:env/route-grants-preview
farthershore env list <business> --format json
farthershore backend create <business> --env route-grants-preview \
--name "Preview API" --slug api --transport direct \
--idempotency-key <persisted-backend-create-attempt-key> \
--origin-url https://preview-api.example.com --default --format json
farthershore backend list <business> --format json
Filter the structured backend list by the preview's environment id.
Test one subscriber on each plan:
- Starter:
GET /v1/jobssucceeds. - Starter:
POST /v1/jobs/createandDELETE /v1/jobs/{id}return the stable authorization denial. - Pro: every declared operation succeeds when the request is otherwise valid.
Client-side hiding is optional presentation. Never treat it as authorization; the route grant is enforced at the gateway.
Common failures
- A plan has too much access: grant the narrow route ref instead of the group.
- A grant has no effect: reuse the exact ref returned by
fs.route(). - The build rejects a ref: do not forge refs or reuse a ref from another compile.
Recover
Change the affected plan's grants, rebuild, and publish a forward contract
revision. Keep stable route paths and action ids when possible.
Agent prompt
Grant these API operations by plan using direct
fs.route()orfs.group()refs. Build and validate, verify both allowed and denied subscribers in preview, and do not publish production.