Docs

End-to-end — scaffold, iterate locally, publish to a tenant.

Extension Authoring

A FastYoke extension is a React bundle the admin shell loads at runtime. The fy CLI handles every mechanical step; this recipe is a full walkthrough from empty directory to installed extension.

1. Scaffold

npm install -g @fastyoke/cli
fy init shift-heatmap
cd shift-heatmap
pnpm add -g @fastyoke/cli
fy init shift-heatmap
cd shift-heatmap

fy init drops you a minimal TypeScript project:

shift-heatmap/
├── manifest.json
├── package.json
├── tsconfig.json
├── README.md
├── src/
│   └── index.tsx
└── .gitignore

The bundler is fy build / fy dev (esbuild under the hood) — no separate config file is generated.

Install deps and you're ready:

npm install

2. Manifest shape

The scaffold's manifest.json is the truth the host reads on install. Minimum required keys:

{
  "id": "acme.shift-heatmap",
  "version": "0.1.0",
  "required_scopes": ["entities:read", "jobs:read"],
  "components": [],
  "pages": [
    { "name": "Shift Heatmap", "path": "/shift-heatmap" }
  ]
}

3. Write a minimal component

Replace src/index.tsx:

import React from 'react';
import { useFastYoke } from '@fastyoke/sdk';

export default function ShiftHeatmap() {
  const { tenantId, jobs } = useFastYoke();

  const [count, setCount] = React.useState<number | null>(null);
  React.useEffect(() => {
    void jobs.list({}).then((rows) => setCount(rows.length));
  }, [jobs]);

  return (
    <section style={{ padding: '2rem' }}>
      <header>
        <p style={{ color: '#6b7280', fontSize: '0.8rem' }}>
          Extension · acme.shift-heatmap
        </p>
        <h1>Shift heatmap</h1>
        <p style={{ color: '#6b7280' }}>Tenant {tenantId}</p>
      </header>
      <p>
        Active jobs in this tenant: {count ?? <em>loading…</em>}
      </p>
    </section>
  );
}

Two things are worth noting:

  • useFastYoke() comes from @fastyoke/sdk and gives you the active tenantId, projectId, and the typed clients (jobs, entities, schemas, pages, files, extensions). The host provides it via an import map, so your extension shares the same React context and SDK clients with the shell.
  • jobs.list() resolves to a JobResponse[] array directly, not a paginated { items } object — read .length straight off the result.
  • We're not importing from react-dom / mounting our own root. The host does that — your component is a child of FastYokeProvider.

4. Local iteration

Spin up watch mode:

fy dev

esbuild rebuilds dist/bundle.mjs on every save. In another terminal, keep the admin shell running (npm run build && npm run preview from the main repo) — every fy dev rebuild reloads seamlessly once the extension is uploaded.

5. Mint a tenant admin JWT

fy publish authenticates as a tenant admin. Grab a token from the admin shell:

# Option A — browser: DevTools → Application → Local Storage →
# fastyoke-auth → copy the "token" value.
# Option B — minted via the /auth/login endpoint.
export FASTYOKE_TOKEN="eyJhbGciOi..."

See Authentication for the full shape.

6. Build + publish

fy build
fy publish \
  --tenant https://fastyoke.example \
  --token "$FASTYOKE_TOKEN" \
  --manifest ./manifest.json \
  --bundle ./dist/bundle.mjs

The upload runs through the three-layer scanner (same policy as manual file uploads). On success, you get back the tenant_extensions row id — the extension is installed and active.

7. Find it in the admin shell

Navigate to Extensions in the admin nav. Your extension shows up with its manifest metadata and the sha256 of the bundle. The page at /shift-heatmap (from your manifest's pages[].path) now serves your component.

Bumping a new version

Two rules:

  1. Bump manifest.jsonversion. Same-version re-upload returns 409 to protect you from overwriting history.
  2. Re-run fy build && fy publish.

The old version row stays in tenant_extensions but is flipped to is_active = 0. The host serves only the new one.

Troubleshooting

SymptomFix
"bundle rejected by security scan"Inspect what the scanner flagged — usually a third-party dep with heuristic-positive code. Swap it or vendor a trimmed copy.
Component renders blankCheck the browser console. Host vs extension React instance mismatch? Run the host via preview, not serve.
Import map not resolvingExtensions must mark react, react-dom, and @fastyoke/sdk as external in the bundler — fy build / fy dev configure esbuild this way out of the box; don't bypass them by running esbuild directly without the same externals.
  • SDK Quickstart — installation + the host-provider pattern explained at a higher level.
  • SDK Reference — full hook + component surface.