@ferscloud/fers-calculation
The engine of FERS: structural finite-element analysis (3D frames, trusses, plates, Eurocode 3 checks) for engineers and AI agents, compiled from Rust to WebAssembly. It runs entirely in the browser or in Node.js — no server round-trip per calculation.
This README covers both published packages:
@ferscloud/fers-calculation(Node.js / server-side,--target nodejs) and@ferscloud/fers-calculation-web(browser / bundler,--target bundler). The API is identical; only installation and bundler setup differ.
Installation
# Browser apps (Vite, Next.js, webpack):
npm install @ferscloud/fers-calculation-web
# Node.js / server-side:
npm install @ferscloud/fers-calculation
Bundler setup (browser build)
@ferscloud/fers-calculation-web is a WASM ES module that initialises via top-level await. Most bundlers need a one-time config:
Vite
npm i -D vite-plugin-wasm
// vite.config.ts
import wasm from "vite-plugin-wasm";
// The package initialises with a top-level await, so the build target has to
// be one that supports it. `esnext` needs no second plugin.
export default {
plugins: [wasm()],
build: { target: "esnext" },
};
esnext keeps the top-level await in the output, which needs Chrome, Edge or
Firefox 89+, or Safari 15+. If you still have vite-plugin-top-level-await in
your config from an earlier version of this README, drop it — recent
@swc/core releases make it fail at vite build.
Next.js / webpack
// next.config.js
module.exports = {
transpilePackages: ["@ferscloud/fers-calculation-web"],
webpack(config) {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
config.output.environment = { ...config.output.environment, asyncFunction: true };
return config;
},
};
The solver is synchronous and CPU-bound; for large models run it inside a Web Worker so the UI thread stays responsive. The Node.js package (@ferscloud/fers-calculation) needs no bundler config.
Quick start — free tier (non-commercial, up to 100 members)
import { calculate_from_json } from "@ferscloud/fers-calculation";
// No init() call needed — the nodejs and bundler builds initialise the
// WASM module automatically on import.
const res = JSON.parse(calculate_from_json(JSON.stringify(myModel)));
if (res.ok) {
const data = res.result; // displacements, member_results, unity_checks, …
} else {
console.error(res.error.code, res.error.message);
}
No API key required. Works for any model with up to 100 members.
Response shape
Every solver call returns a JSON envelope, so you JSON.parse once and branch on ok — you never have to sniff whether the returned string "looks like" an error:
// success
{ "ok": true, "result": { /* the full result document */ } }
// failure (invalid model, over the member limit, malformed JSON, …)
{ "ok": false, "error": { "code": "LimitExceeded",
"message": "Number of members (250) exceeds allowed maximum of 100" } }
error.code is one of InvalidJson, LimitExceeded, SolveError, InternalPanic, InternalSerialization. The return value is always valid JSON.
Authenticated tier — Pro (up to 10 000 members)
Pro limits are unlocked by passing a short-lived signed token issued by the FERS Cloud server. The token is verified inside the WASM using an Ed25519 public key baked into the binary — it cannot be forged.
How it works
Your server → POST https://ferscloud.com/api/solver/token → signed 30-min token
(X-API-Key: <your-api-key>)
Browser → calculate_from_json_with_token(model, token) → Pro limits unlocked
1. Get an API key
Log in at ferscloud.com, go to Profile → API Keys, and create a key. Store it as an environment variable on your server — never in browser code.
2. Backend: fetch a solve token
Your server fetches and caches the token. Example for a Next.js API route:
// pages/api/solve-token.ts
const FERS_API_KEY = process.env.FERS_API_KEY!;
let cached: { token: string; expiresAt: number } | null = null;
export default async function handler(req, res) {
const now = Date.now();
// Re-use if more than 5 minutes remain
if (cached && cached.expiresAt - now > 5 * 60 * 1000) {
return res.json({ token: cached.token });
}
const resp = await fetch("https://ferscloud.com/api/solver/token", {
method: "POST",
headers: { "X-API-Key": FERS_API_KEY },
});
if (!resp.ok) return res.status(502).json({ error: "Token fetch failed" });
const { token, expiresAt } = await resp.json();
cached = { token, expiresAt: new Date(expiresAt).getTime() };
return res.json({ token });
}
Same pattern works for Express, Deno, Bun, Cloudflare Workers, or any server-side runtime.
3. Frontend: call the solver with the token
import { calculate_from_json_with_token } from "@ferscloud/fers-calculation";
const { token } = await fetch("/api/solve-token").then(r => r.json());
const res = JSON.parse(calculate_from_json_with_token(JSON.stringify(myModel), token));
if (!res.ok) throw new Error(`${res.error.code}: ${res.error.message}`);
const data = res.result;
If the token is missing, expired, or invalid, the solver falls back to the free 100-member limit — it never throws (a genuine solve failure comes back as { ok: false, error }, not an exception).
Tier comparison
| Feature | Free | Pro | Enterprise |
|---|---|---|---|
| Use | Non-commercial | Commercial and non-commercial | Commercial and non-commercial, by agreement |
| Max members | 100 | 10 000 | 1 000 000 |
| WASM function | calculate_from_json | calculate_from_json_with_token | calculate_from_json_with_token |
| Requires token | No | Yes | Yes |
| Token TTL | — | 30 minutes | 30 minutes |
An Enterprise token is a Pro token that also carries "plan":"enterprise"; engines before 0.2.78 read it as Pro. In the browser, memory runs out long before 1 000 000 members: the WASM build has 4 GB and one thread.
Several cores: solving in parts
One WASM instance solves on one thread. To spread a model's load combinations over several workers (Web Workers in a browser, worker_threads in Node), give every worker the same model string and its own part, and merge what they return:
// worker.js: one per core
import { calculate_part_from_json } from "@ferscloud/fers-calculation-web";
self.onmessage = ({ data: { model, index, count } }) => {
self.postMessage(calculate_part_from_json(model, index, count));
};
// main thread
import { merge_parts_from_json } from "@ferscloud/fers-calculation-web";
const model = JSON.stringify(myModel); // one string, the same bytes everywhere
const count = workers.length;
const parts = await Promise.all(
workers.map((worker, index) => solveIn(worker, { model, index, count })) // resolves with the worker's string
);
const res = JSON.parse(merge_parts_from_json(model, parts)); // what calculate_from_json returns
- The merged envelope is byte for byte what
calculate_from_jsonreturns for the same model. If a part failed, the merge returns that part's error envelope, the one a single solve would have stopped on. - Pass the parts unopened and in index order. The merge reads them itself; parsing them in JavaScript only costs time.
- Only the combinations are divided. Every part reads the whole model and, whenever one solve would, solves the load cases, so each combination starts where it would in one solve; part 0 also reports the load cases and runs the modal, buckling and seismic analyses. Combinations that share a name always fall in the same part.
- Each worker holds its own copy of the model and of its solve, so memory grows with the number of workers.
- With a solve token, use
calculate_part_from_json_with_token(model, token, index, count)in the workers andmerge_parts_from_json_with_token(model, token, parts)to merge.
Security note
Keep your FERS_API_KEY server-side only. The solve token it fetches is safe to pass to the browser — it is short-lived and cryptographically signed. Only FERS Cloud can issue valid tokens; the browser WASM can only verify them.
TypeScript types
The browser package ships generated model types alongside the function signatures:
import type { FERS, ResultsBundle } from "@ferscloud/fers-calculation-web/fers-models";
FERS is the input model; ResultsBundle is the result payload of a successful envelope. Both are generated from the engine's OpenAPI schema, so they track the published version.
Deflected shape (optional)
Set include_member_deflected_shape in the model's analysis options to get a
ready-to-plot, load-exact deflected shape per member — the member's global
displacement sampled along its length — instead of reconstructing the curve yourself:
const model = {
/* … model + load cases … */
analysis: { /* … */ options: { /* … */ include_member_deflected_shape: true } },
};
const res = JSON.parse(calculate_from_json(JSON.stringify(model)));
const mr = res.result.results.loadcases["…"].member_results["1"];
// mr.member_displacements: [{ x_frac, displacement: [dx, dy, dz] }, …]
// x_frac 0→1 along the member; displacement in the global input frame.
Off by default to keep the payload lean; omitted from member_results when not requested.
Attribution
Free-tier results carry an attribution object minted inside the solver.
Premium results (solved with a valid solve token) do not, so the same code
credits FERS on the free tier and white-labels on Pro:
import { fersAttributionHtml } from "@ferscloud/fers-calculation-web/badge";
// "" when the result came back from a Pro solve.
const credit = fersAttributionHtml(res);
getFersAttribution, fersAttributionText and createFersBadge are also
exported. All of them accept the envelope, the parsed model or the results
bundle, and all return nothing for a Pro result or an error envelope.
On 0.2.61 this subpath resolved only under a bundler; plain Node ESM needed
/badge.js. From 0.2.62 the package ships an exports map and both forms work
everywhere.
Using the free tier in a non-commercial application? Please display the credit.
Licence
From 0.2.70 the engine is licensed under the PolyForm Strict License 1.0.0
(see LICENSE). It is free for any noncommercial purpose: study, teaching,
research, hobby projects, and use by schools, charities and public bodies.
Models of more than 100 members need a Pro solve token.
Commercial use, at any model size, needs a paid FERS Cloud plan, which includes the commercial licence. The licence does not allow modifying or redistributing the package.
Links
- Structural analysis in JavaScript — why run the solver client-side
- JavaScript documentation — bundler setup, error codes, types, solve tokens
- Full documentation — every route into FERS
- llms-full.txt — the whole documentation in one fetch, for agents
- FERS Cloud
- FERS Python package —
pip install FERS