Skip to content

Components ​

A component is a building block somebody else wrote, installed alongside fft: a binary plus a manifest that adds commands to the CLI, or teaches the emulator to deliver events to a broker fft has never heard of. Once installed, its commands appear in fft --help like any other.

What is installed here ​

sh
fft component list
fft component info weather

STATUS is installed when the binary is present. It is available when fft knows about the component but it has not been installed — which only ever applies to a component fft ships itself, because those are registered whether or not they are present. So a command can exist in --help, and in this document, and still need one install before it runs.

ORIGIN says who wrote it: first-party for one fft ships, community for anything else. It is the column that decides how much the others are worth trusting.

The same list, with install, upgrade and remove on it, is the Components screen of fft tui — useful to a person, not to you: drive the commands directly.

The emulator is the reference first-party component: it used to be built into fft and is now installed separately, which is what took a third off the CLI's download. Its two broker transports — emulator-pubsub and emulator-servicebus — are components too. See the emulator.

Installing ​

sh
fft component install emulator
fft component install acme/fft-weather
fft component install acme/fft-weather@v1.2.0
fft component install --path ./my-component

fft fetches the release, checks the archive against that release's checksums.txt, and unpacks it into a staging directory. Nothing is installed until the user says yes, and the question shows where it came from, whether the download matched, and what the component will be allowed to do.

Do not answer that question for the user. -y exists, and it is theirs to pass: a component runs as whoever ran fft, with whatever access they have. Installing one is a decision about trust, and it is not yours to make on their behalf.

sh
fft component upgrade weather
fft component remove weather

What a component is given ​

fft builds the environment its components run with. What lands in it is decided by the session each command declares, and nothing else:

sessionwhat the component gets
noneno tenant credential at all — not even the base URL
readthe base URL and a short-lived id token, with FFT_READ_ONLY forced on
writethe same, without the forced read-only

Every FFT_* variable in the caller's environment is stripped before this is built, so a component never inherits a password or an API key that happened to be exported. The Firebase API key is never handed over at any level: a component gets a credential that expires, never the one that mints more.

fft component info <name> prints all of it — which commands, which session each one asks for, whether it declares that it changes data. That is the whole of what fft knows about a component before running it, so it is the whole of what there is to review.

The read-only gate still applies ​

A component command that declares mutates is refused against a read-only project, with exit code 10, before the component is started. So is one that claims an operation the API spec says is a write, whatever its manifest declares about itself.

What the gate cannot catch is a component that writes while declaring nothing and claiming nothing. Say so plainly if the user asks: the manifest is a declaration fft enforces, not a sandbox it imposes.

Reporting on one ​

Two things are worth saying when a user asks about a component:

sh
fft component info weather -o json
  • Whether it is first-party. A community component is somebody else's code running with the user's credentials.
  • Which session its commands ask for. A write session on a component nobody recognises is worth raising before it is installed, not after.

When a command is missing ​

If a component's command reports that it is not installed, the fix is the command in the error itself — install the component it names:

sh
fft component install emulator

If fft --help warns that a component declared a command fft already has, the built-in one wins and the component's is dropped. That is deliberate — a component cannot silently change what an existing command does — and the component's author is the one who has to rename it.

Scaffolding a new component ​

fft component init stamps a runnable skeleton into <dir>/<name>, so an author starts from a working component rather than a blank manifest:

sh
fft component init pricing
fft component init pricing --lang go
fft component init ship --kind transport
fft component init ship --kind transport --lang python
  • --kind command (the default) adds fft <name>; its skeleton prints the arguments and the FFT_ environment it was handed — with the tenant token masked, since stdout is the stream the output contract keeps safe to pipe — so the process contract is visible before any logic is written. --kind transport delivers emulator events; its skeleton answers the protocol's hello, refuses plan, and acks send.
  • --lang is shell, go, python or node. shell, python and node are interpreter scripts that run the moment they are installed; go compiles to bin/, so it needs go build first (the emitted README says so). The default is shell for a command and go for a transport.
  • --session none|read|write (command only) sets the tenant session the command receives; a write skeleton is marked mutates so the read-only gate applies to it.

The emitted component.yaml is validated exactly as fft component install validates one, so anything init produces is a manifest fft component install accepts. A shell, python or node command installs straight away; a --lang go component has to be built first, so that the executable its manifest names exists (the emitted README says so):

sh
fft component init pricing
fft component install --path ./pricing

after which fft pricing runs the component and prints its args and the FFT_ environment.

Writing a transport component in Go ​

A transport component is a separate process the emulator talks to over stdin and stdout: newline-delimited JSON, one hello/plan/send request per line, stderr for logs. Any language can implement that wire format directly. Go authors do not have to — the protocol types and the loop that drives them are a public package, github.com/Joessst-Dev/fft-cli/pkg/transportproto.

Implement transportproto.Handler and hand it to transportproto.Serve, which owns the read/answer/write loop and returns nil at EOF (the emulator closing stdin is how it says it is done):

go
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"strconv"
	"syscall"

	"github.com/Joessst-Dev/fft-cli/pkg/transportproto"
)

// myTransport would hold a broker client; it answers with constants here so the
// example stays about the wiring.
type myTransport struct{}

// Hello reports the target types this transport delivers, and one line for the
// emulator's startup notice saying where.
func (t *myTransport) Hello() (targets []string, status string, err error) {
	return []string{"MY_BROKER"}, "publishing to my-broker at localhost:9000", nil
}

// Plan resolves one subscription target into the label its deliveries are reported
// under, or returns an error the emulator logs as the reason it skips that subscription.
func (t *myTransport) Plan(target map[string]any) (label string, err error) {
	return "my-broker/orders", nil
}

// Send delivers one event to a target Plan accepted.
func (t *myTransport) Send(ctx context.Context, target map[string]any, event string, data []byte) error {
	return nil
}

func main() {
	// fft does not enforce the protocol version — it only reports the one it speaks in
	// FFT_TRANSPORT_API — so refusing a host this build does not understand is the
	// component's own job.
	if got := os.Getenv(transportproto.EnvVersion); got != "" && got != strconv.Itoa(transportproto.Version) {
		fmt.Fprintf(os.Stderr, "the emulator speaks transport protocol %s, this component speaks %d\n", got, transportproto.Version)
		os.Exit(1)
	}

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	if err := transportproto.Serve(ctx, os.Stdin, os.Stdout, &myTransport{}); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

This mirrors the Example in the package's example_test.go, which compiles under go test and renders on pkg.go.dev. The first-party emulator-pubsub and emulator-servicebus transports are built on the same package and are the working reference for wiring up a real broker client, validating targets, and shutting down cleanly.

An independent open-source project — not affiliated with, endorsed by, or supported by fulfillmenttools.