@finstreet/secure-fetch — Complete Guide
The @finstreet/secure-fetch library provides a fully type-safe approach to backend requests using Zod for schema validation of payloads, path variables, and responses.
Path Resolution
Before creating any files, invoke the finstreet-fe:path-resolver skill with your input parameters (featureName, featureType, product, role) to resolve the correct paths. Use the returned Feature Path with /backend appended as {featurePath} in the file structure below.
File Structure
Every feature's backend integration lives in exactly three files:
{featurePath}/
├── schema.ts ← all Zod schemas and exported types
├── server.ts ← server-side request functions (default)
└── client.ts ← client-side request functions (only when needed)
NEVER create additional files or directories. If these files already exist, update them.
Interactive Discovery Workflow
When the user provides a search keyword (e.g. "inquiries") and a repo (e.g. eco-scale-bfw or eco-scale-cb), follow this interactive flow:
Step 1 — Search for endpoints
Call the search-swagger-documentation MCP tool with the user's keyword and repo. Present the returned endpoints as a numbered list so the user can see what's available.
Step 2 — Confirm endpoint selection
Ask the user which endpoints to implement. They might say "all of them", pick specific numbers, or exclude some. Keep asking until they're satisfied with the selection.
Step 3 — Configure each endpoint
For each selected endpoint, ask the user:
- Protected? (default:
true) — whether the request requires authentication - Server or client? (default:
server) — server.ts for SSR/actions, client.ts for polling/dynamic client-side calls - Result schema needed? (default:
yesfor GET,nofor non-GET) — whether to generate a result schema
Present these as a table or list with defaults so the user can confirm quickly (e.g. "All look good" or "Change endpoint 3 to client").
Step 4 — Fetch full documentation and implement
For each confirmed endpoint, call the get-swagger-documentation MCP tool with the repo and the specific path to get the full Swagger details. Then implement following the steps below:
- Check the existing
schema.tsfor reusable schemas - Build the required schemas in
schema.ts - Build the endpoint config and service export in the appropriate file (
server.tsorclient.ts)
Direct usage
If the user already provides all the details upfront (repo, path, protected, server/client), skip the interactive flow and go straight to implementation.
Key Imports
// schema.ts
import * as z from "@/lib/zod"; // or from "zod"
// server.ts
import { EndpointConfig } from "@finstreet/secure-fetch";
import { createServerFetchFunction } from "@/shared/backend/createServerFetchFunction";
import { createPaginatedServerFetchFunction } from "@/shared/backend/createServerFetchFunction";
// client.ts
import { EndpointConfig } from "@finstreet/secure-fetch";
import { createClientFetchFunction } from "@/shared/backend/createClientFetchFunction";
Step-by-Step Reference
- For building schemas (pathVariables, result, payload, paginated), see schema.md
- For building endpoint configs and service exports (server/client, paginated), see endpoint-config.md
Permissions Are Not a Normal Endpoint
GET /permissions is already wired and is not yours to add or call. It is fetched once per
login by loadRailsPermissions in src/lib/railsAuthPlugin.ts — a raw fetch, not a
secure-fetch function — and the result is stored on the better-auth session by src/auth.ts.
- To read permissions, call
authPermissions()from@/shared/auth/permissions. It reads the session, costs no request, and is the right thing to use in a page, layout, or server action. - Never create or call a secure-fetch function for
/permissions. It would re-request on every render, and the session copy is what the app actually gates on. If you find one inserver.ts, it is dead — leave it alone, don't wire it up. - When the backend adds a permission field, the only change is
PermissionsSchemainsrc/shared/backend/models/auth/schema.ts. Noserver.tsentry, and no pairedmock-apitask — the auth plugin's raw fetch never passes through the mock registry.
Adding a permission field
A new field on PermissionsSchema MUST be declared optional, and read with optional chaining at
every hop:
permissions?.comment?.create; // correct
permissions?.comment.create; // compiles, throws at runtime
The plugin casts rather than parses, so a required field type-checks as present while being
undefined at runtime, and auth.ts caches the session for 24h — live sessions lack a newly
shipped field for up to a day after the backend deploys. Optionality is permanent, not a launch
workaround.
What Permissions Are and Are Not For
Permissions gate entry — whether an action is offered at all. They do NOT gate content. Reach for one only when there is no request whose answer you could use instead:
- Never gate a fetch on a permission. Call the endpoint; the request is the answer. A backend
that says no returns 403 and
fetchWithErrorHandlingdeals with it. - Never let a permission decide whether a section renders. An empty list renders as an empty list, not as a missing panel.
- Per-item capability comes from the response. If a list carries
editable/deletableor similar per-entry flags, those are the source of truth for that entry's actions — do not add a parallel permission field for the same decision. - Do gate an action the user would otherwise complete before being refused. Nothing tells you
whether a user may create something until they submit, and finding out via 403 after they have
written the comment is not acceptable. A
createpermission is the right gate for a create button; aneditableflag the response already gave you is not something a permission should second-guess.
Rules
- The user MUST specify which repo to use (e.g.
eco-scale-bfw,eco-scale-cb). If not provided, ask. - ALWAYS convert
snake_casetocamelCasein schemas AND in path templates protected: trueunless the user explicitly chooses otherwise during configuration- Only add a
resultSchemafor GET requests (or if explicitly required) - Only add a
payloadSchemafor non-GET requests - Use
server.tsby default; only useclient.tsfor polling or dynamic client-side requests - Do NOT run any
tscorpnpmcommands after implementation - NEVER create a secure-fetch function for
/permissions— see "Permissions Are Not a Normal Endpoint"