Boot a local FastYoke backend (npx or Docker), provision a deal-tracker FSM, and wire up a React app with the SDK — no account needed.
Quick start
This walkthrough takes you from zero to a running React app backed by a
real FastYoke workflow, entirely on your own machine — no sign-up, no
cloud tenant. You'll build a tiny "deal tracker": a list of deals, each
one a job moving through a three-state pipeline (New → Working →
Won), driven by the FastYoke FSM engine and rendered with
@fastyoke/sdk React hooks.
What you'll build
A single-page app that lists a handful of deals and shows each one's
current pipeline state next to a button for the next valid transition.
Clicking Advance moves a deal from New to Working; clicking
Win moves it from Working to Won. Every click is a real FSM
transition against a local backend — there's no mock data layer.
Along the way you'll touch the three primitives every FastYoke app is built from:
- Entities — your domain records (here, a
dealwith anameand anamount), stored and fetched through the tenant entity API. - FSM jobs — a
deal_lifecycleschema with two transitions (advance,win) driving each deal's job through its states. - The SDK —
@fastyoke/sdk's React hooks (useEntities,useJobs,useTransitionJob) that read and drive both of the above.
You have two ways to run the backend locally — pick one:
- npx runtime (light) — a zero-install Node sidecar. Fastest to try, but it's a subset of the platform (no PDF, scripting, extensions, e-signature, or marketplace apps).
- Docker full engine — the complete engine in one container. Same FSM/entities/forms core, plus everything the light runtime leaves out.
The steps below are identical in shape for both, but the exact commands diverge until you get to the React app, which is one shared codebase for either backend.
Prerequisites
- Node.js 18+ and
npm. - Docker — only if you're taking the Docker full-engine path. The npx path needs nothing beyond Node.
- No FastYoke account, sign-up, or API key required for either path.
Step 1 — Start a local backend
Pick one.
Option A: npx runtime (light)
npx --yes fastyoke@latest init
npx --yes fastyoke@latest dev
init scaffolds a .fastyoke/ directory (config, an auth key, a seed
file, and uploads) in your current directory. dev boots the sidecar
against an on-disk SQLite database:
Server listening at http://127.0.0.1:8787
This is a subset of the platform — no PDF rendering, WASM scripting tier, extensions, e-signature, or marketplace apps. It covers the FSM/entities/forms core, which is everything this walkthrough needs.
Base URL for this path: http://127.0.0.1:8787.
Option B: Docker full engine
docker run -d --name fy-qs -p 8080:8080 -e DEPLOY_ENV=sandbox -e PORT=8080 ghcr.io/fastyoke/backend:sandbox
DEPLOY_ENV=sandbox runs against a plain SQLite database with
third-party integrations (payments, email, external SSO) pointed at
local stubs, so nothing leaves your machine. Give it a second to come
up, then confirm it's healthy:
until curl -sf http://127.0.0.1:8080/api/v1/health >/dev/null; do sleep 1; done
curl -s http://127.0.0.1:8080/api/v1/health
# {"service":"fastyoke","status":"ok"}
Base URL for this path: http://127.0.0.1:8080.
Step 2 — Get a token and tenant id
How you get an admin token and a tenant id depends on which backend you started.
Option A: npx runtime
There's no sign-up call at all — init/dev already wrote a token to
disk, and the tenant id is always the fixed string local:
cat .fastyoke/token
export FASTYOKE_API_URL=http://127.0.0.1:8787
export FASTYOKE_TOKEN=$(cat .fastyoke/token)
export FASTYOKE_TENANT_ID=local
Option B: Docker full engine
Sign up against the sandbox backend — turnstile_token: "sandbox" works
because the sandbox build runs an accept-all Turnstile verifier:
curl -s -X POST http://127.0.0.1:8080/api/v1/auth/signup \
-H 'content-type: application/json' \
-d '{"email":"dev@local.test","password":"DemoPassword123!","org_name":"Deal Co","org_legal_name":"Deal Co","country":"US","turnstile_token":"sandbox"}'
The response includes a jwt and a memberships array — use the first
membership's tenant_id:
{
"jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
"memberships": [ { "tenant_id": "f91d0a96-7153-4b18-af67-3bd073003640", "...": "..." } ]
}
export FASTYOKE_API_URL=http://127.0.0.1:8080
export FASTYOKE_TOKEN=<the jwt from the response>
export FASTYOKE_TENANT_ID=<memberships[0].tenant_id from the response>
Step 3 — Provision the deal-tracker schema
This is the last step where the two paths differ — everything from here on is the same code against either backend.
Option A: npx runtime
There's no schema API on this backend — FSM schemas only come from
.fastyoke/seed.json. Open it and merge in a deal entity kind, two
sample deal records, the deal_lifecycle schema, and two jobs:
{
"entity_kinds": [
{
"name": "deal",
"schema": {
"fields": [
{ "key": "name", "type": "string" },
{ "key": "amount", "type": "number" }
]
}
}
],
"entity_records": {
"deal": [
{ "id": "deal_acme", "data_payload": { "name": "Acme", "amount": 5000 } },
{ "id": "deal_globex", "data_payload": { "name": "Globex", "amount": 12000 } }
]
},
"fsm_schemas": [
{
"id": "sch-deal-lifecycle",
"name": "deal_lifecycle",
"entity_name": "deal",
"schema_json": {
"initial_state": "New",
"transitions": [
{ "event": "advance", "from": "New", "to": "Working" },
{ "event": "win", "from": "Working", "to": "Won" }
]
}
}
],
"jobs": [
{ "id": "job-deal_acme", "schema_id": "sch-deal-lifecycle", "current_state": "New", "context_record_id": "deal_acme" },
{ "id": "job-deal_globex", "schema_id": "sch-deal-lifecycle", "current_state": "New", "context_record_id": "deal_globex" }
]
}
Merge these keys into your existing .fastyoke/seed.json (add to the
existing arrays/objects rather than replacing the file), then restart
the sidecar to apply it — hot-reload is best-effort, a clean restart
reliably picks up seed edits. Stop the running dev process first
(Ctrl-C in the terminal where it's running), then start it again:
npx --yes fastyoke@latest dev
Option B: Docker full engine
This backend has a real schema API. Save the following as
provision.mjs and run it:
// provision.mjs — installs deal_lifecycle + a few New deals. Run: node provision.mjs
const API = process.env.FASTYOKE_API_URL, TOKEN = process.env.FASTYOKE_TOKEN, TENANT = process.env.FASTYOKE_TENANT_ID;
const api = (path, body) => fetch(`${API}${path}`, {
method: body ? 'POST' : 'GET',
headers: { 'content-type': 'application/json', authorization: `Bearer ${TOKEN}` },
body: body ? JSON.stringify({ tenant_id: TENANT, ...body }) : undefined,
}).then(async r => { if (!r.ok) throw new Error(`${path} → ${r.status}: ${await r.text()}`); return r.json(); });
const schema = await api('/api/v1/tenant/schemas', {
name: 'deal_lifecycle', entity_name: 'deal',
schema_json: { initial_state: 'New', states: ['New', 'Working', 'Won'],
transitions: [ { event_type: 'advance', from: 'New', to: 'Working' },
{ event_type: 'win', from: 'Working', to: 'Won' } ] },
});
for (const d of [{ name: 'Acme', amount: 5000 }, { name: 'Globex', amount: 12000 }]) {
const rec = await api('/api/v1/tenant/entities/deal', { data_payload: d });
await api('/api/v1/tenant/jobs', { schema_id: schema.id, context_record_id: rec.id });
}
console.log('provisioned deal_lifecycle + seed deals');
node provision.mjs
That's the last forked step — the React app below works unchanged against whichever backend you provisioned.
Step 4 — Build the front end
Scaffold a Vite + React + TypeScript app:
npm create vite@latest deal-tracker-app -- --template react-ts
cd deal-tracker-app
Now install the SDK:
npm i @fastyoke/sdk @fastyoke/sdk-core
Replace src/App.tsx with:
import { FastYokeProvider, useEntities, useJobs, useTransitionJob } from '@fastyoke/sdk';
import type { EntityResponse, PagedEntityResponse } from '@fastyoke/sdk-core';
const cfg = {
baseUrl: import.meta.env.VITE_FASTYOKE_API_URL as string,
tenantId: import.meta.env.VITE_FASTYOKE_TENANT_ID as string,
token: import.meta.env.VITE_FASTYOKE_TOKEN as string,
};
const fetcher: typeof fetch = (input, init = {}) =>
fetch(input, {
...init,
headers: { ...init.headers, authorization: `Bearer ${cfg.token}` },
});
export default function App() {
return (
<FastYokeProvider baseUrl={cfg.baseUrl} tenantId={cfg.tenantId} fetcher={fetcher}>
<DealList />
</FastYokeProvider>
);
}
// The npx light-runtime's GET /tenant/entities/:kind returns
// `{ rows, total }`; the Docker full-engine (and the @fastyoke/sdk-core
// `PagedEntityResponse` type) returns `{ records, total, page, page_size }`.
// Read both keys so the same component works against either backend.
function recordsOf(data: PagedEntityResponse | null): EntityResponse[] {
if (!data) return [];
const anyData = data as PagedEntityResponse & { rows?: EntityResponse[] };
return anyData.records ?? anyData.rows ?? [];
}
function DealList() {
const { data, loading, error } = useEntities('deal', { pageSize: 50 });
if (loading) return <p>Loading…</p>;
if (error) return <p>{String(error)}</p>;
const deals = recordsOf(data);
return (
<ul>
{deals.map((d) => (
<DealRow key={d.id} deal={d} />
))}
</ul>
);
}
// The FSM's `event_type` for a given `current_state`. `deal_lifecycle` is
// New -> (advance) -> Working -> (win) -> Won; Won has no outgoing edge.
function nextEvent(state: string): { label: string; eventType: string } | null {
if (state === 'New') return { label: 'Advance', eventType: 'advance' };
if (state === 'Working') return { label: 'Win', eventType: 'win' };
return null;
}
function DealRow({ deal }: { deal: EntityResponse }) {
// The SDK has no "find job by record id" hook, but `useJobs` accepts
// `entityId`, which the backend maps to `jobs.context_record_id` — the
// way to find *this* deal's job. Confirmed on both backends.
const { data: jobs, loading: jobsLoading, refetch } = useJobs({ entityId: deal.id });
const { transitionJob, loading: transitioning, error: transitionError } =
useTransitionJob();
const job = jobs?.[0];
const state = job?.current_state ?? '…';
const next = job ? nextEvent(job.current_state) : null;
return (
<li>
{String(deal.data_payload.name)} — <strong>{jobsLoading ? '…' : state}</strong>
{next && job && (
<button
disabled={transitioning}
onClick={async () => {
await transitionJob({ id: job.id, input: { eventType: next.eventType } });
refetch();
}}
>
{transitioning ? 'Working…' : next.label}
</button>
)}
{transitionError && <span style={{ color: 'crimson' }}> {String(transitionError)}</span>}
</li>
);
}
Set your environment variables in .env:
Option A: npx runtime
VITE_FASTYOKE_API_URL=
VITE_FASTYOKE_PROXY_TARGET=http://127.0.0.1:8787
VITE_FASTYOKE_TENANT_ID=local
VITE_FASTYOKE_TOKEN=<contents of .fastyoke/token>
Option B: Docker full engine
VITE_FASTYOKE_API_URL=http://127.0.0.1:8080
VITE_FASTYOKE_TENANT_ID=<memberships[0].tenant_id from signup>
VITE_FASTYOKE_TOKEN=<jwt from signup>
Step 5 — Run it
npm run dev
Open the printed local URL in your browser. You'll see the two seeded
deals, Acme and Globex, each showing New next to an Advance
button. Click it — the button briefly shows "Working…" while the
transition request fires, then the row updates to Working and the
button becomes Win. Click Win and the deal settles into Won,
its terminal state with no further button.
Every click is a real POST .../jobs/:id/transition call evaluated by
the FSM engine and appended to that job's event log — there's no mock
state in the app itself.
What's next
The app you just built runs unchanged against any of FastYoke's three run modes — only the base URL (and how you obtain a token) changes:
- npx runtime → Docker full engine: point
VITE_FASTYOKE_API_URLathttp://127.0.0.1:8080and use the Docker token/tenant id from Step 2's signup call. Do this once you need a feature the light runtime doesn't have — PDF rendering, the WASM scripting tier, extensions, e-signature, or marketplace apps. - Docker full engine → On-Prem: the same container image deploys for production self-hosting behind your own firewall. See On-Prem.
- Either → Managed Cloud: swap the base URL to your tenant's
api.fastyoke.comendpoint and sign up for real. See Getting started.
For the bigger picture on how the three modes compare, see the Runtime overview.
Where to go from here:
- App spec reference — the full schema format for entities, FSM schemas, and forms, beyond what this walkthrough used.
- SDK: build a CRUD UI — a deeper recipe for entity list/detail/edit screens with the SDK.
- Tutorials — end-to-end build flows, from a first API integration to authoring an extension.
- Error envelope, Idempotency, Tenant scoping, and Rate limits — the day-1 platform invariants every API consumer needs, regardless of which run mode you're targeting.