Docs

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 (NewWorkingWon), 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 deal with a name and an amount), stored and fetched through the tenant entity API.
  • FSM jobs — a deal_lifecycle schema 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_URL at http://127.0.0.1:8080 and 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.com endpoint 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: