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
fft component list
fft component info weatherSTATUS 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
fft component install emulator
fft component install acme/fft-weather
fft component install acme/fft-weather@v1.2.0
fft component install --path ./my-componentfft 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.
fft component upgrade weather
fft component remove weatherWhat 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:
| session | what the component gets |
|---|---|
none | no tenant credential at all — not even the base URL |
read | the base URL and a short-lived id token, with FFT_READ_ONLY forced on |
write | the 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:
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
writesession 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:
fft component install emulatorIf 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:
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) addsfft <name>; its skeleton prints the arguments and theFFT_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 transportdelivers emulator events; its skeleton answers the protocol'shello, refusesplan, and ackssend.--langisshell,go,pythonornode.shell,pythonandnodeare interpreter scripts that run the moment they are installed;gocompiles tobin/, so it needsgo buildfirst (the emittedREADMEsays so). The default isshellfor a command andgofor a transport.--session none|read|write(command only) sets the tenant session the command receives; awriteskeleton is markedmutatesso 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):
fft component init pricing
fft component install --path ./pricingafter 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):
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.