Page not found :(
+The page you are looking for doesn't exist or has been moved.
+diff --git a/.keepme b/.keepme new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/404.html b/404.html new file mode 100644 index 0000000000..12181db020 --- /dev/null +++ b/404.html @@ -0,0 +1,211 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +The page you are looking for doesn't exist or has been moved.
+FTL is tooling, runtimes, and frameworks for simplifying the creation of distributed systems.
+ + + + + +One page summary of how to start a new FTL project.
+Install the FTL CLI via Hermit, Homebrew, or manually.
+FTL can be installed from the main Hermit package repository by simply:
+hermit install ftl
+
+Alternatively you can add hermit-ftl to your sources by adding the following to your Hermit environment's bin/hermit.hcl
file:
sources = ["https://github.com/TBD54566975/hermit-ftl.git", "https://github.com/cashapp/hermit-packages.git"]
+
+brew tap TBD54566975/ftl && brew install ftl
+
+Download binaries from the latest release page and place them in your $PATH
.
The FTL VSCode extension will run FTL within VSCode, and provide LSP support for FTL, displaying errors within the editor.
+Once FTL is installed, initialize an FTL project:
+mkdir myproject
+cd myproject
+ftl init myproject . --hermit
+
+This will create an ftl-project.toml
file, a git repository, and a bin/
directory with Hermit tooling.
Now that you have an FTL project, create a new module:
+ftl new go . alice
+
+This will place the code for the new module alice
in myproject/alice/alice.go
:
package alice
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK.
+)
+
+type EchoRequest struct {
+ Name ftl.Option[string] `json:"name"`
+}
+
+type EchoResponse struct {
+ Message string `json:"message"`
+}
+
+//ftl:verb
+func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) {
+ return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil
+}
+
+Each module is its own Go module.
+Any number of modules can be added to your project, adjacent to each other.
+If using VSCode, opening the directory will prompt you to start FTL:
+ +Alternatively start the local FTL development cluster from the command-line:
+ +This will build and deploy all local modules. Modifying the code will cause ftl dev
to rebuild and redeploy the module.
FTL has a console that allows interaction with the cluster topology, logs, traces, +and more. Open a browser window at https://localhost:8892 to view it:
+ +You can call verbs from the console:
+ +Or from a terminal use ftl call
to call your verb:
Create another module and call alice.echo
from it with:
//ftl:verb
+import "ftl/alice"
+
+out, err := ftl.Call(ctx, alice.Echo, alice.EchoRequest{})
+
+Explore the reference documentation.
+ + + + + +Answers to frequently asked questions.
+Because of the nature of writing FTL verbs and data types, it's easy to think of it as just writing standard native code. Through that lens it is then somewhat surprising when FTL disallows the use of arbitrary external data types.
+However, FTL types are not just native types. FTL types are a more convenient method of writing an IDL such as Protobufs, OpenAPI or Thrift. With this in mind the constraint makes more sense. An IDL by its very nature must support a multitude of languages, so including an arbitrary type from a third party native library in one language may not be translatable to another language.
+There are also secondary reasons, such as:
+So what to do? See the external types documentation +for how to work around this limitation.
+In its least abstract form, a module is a collection of verbs, and the resources (databases, queues, cron jobs, secrets, config, etc.) that those verbs rely on to operate. All resources are private to their owning module.
+More abstractly, the separation of concerns between modules is largely subjective. You can think of each module as largely analogous to a traditional service, so when asking where the division between modules is that could inform your decision. That said, the ease of deploying modules in FTL is designed to give you more flexibility in how you structure your code.
+FTL's type system includes support for optionals. In Go this is represented as ftl.Option[T]
, in languages with first-class support for optionals such as Kotlin, FTL will leverage the native type system.
When FTL is mapping to JSON, optional values will be represented as null
.
In Go specifically, pointers to values are not supported because pointers are semantically ambiguous and error prone. They can mean, variously: "this value may or may not be present", or "this value just happens to be a pointer", or "this value is a pointer because it's mutable"
+Additionally pointers to builtin types are painful to use in Go because you can't obtain a reference to a literal.
+This is currently due to FTL relying on traditional schema evolution for forwards/backwards compatibility - eg. changing a slice to a struct in a backward compatible way is not possible, as an existing deployed peer consuming the slice will fail if it suddenly changes to a data structure.
+Eventually FTL will allow multiple versions of a verb to be simultaneously deployed, such that a version returning a slice can coexist temporarily with a version returning a struct. Once all peers have been updated to support the new type signature, the old version will be dropped.
+Verbs and types can only be exported from the top level of each module. You are welcome to put any helper code you'd like in a nested package/directory.
+FTL supports the following types: Int
(64-bit), Float
(64-bit), String
, Bytes
(a byte array), Bool
, Time
, Any
(a dynamic type), Unit
(similar to "void"), arrays, maps, data structures, and constant enumerations. Each FTL type is mapped to a corresponding language-specific type. For example in Go Float
is represented as float64
, Time
is represented by time.Time
, and so on.
Note that currently (until type widening is implemented), external types are not supported.
+For example:
+# ftl dev ~/src/ftl
+info: Starting FTL with 1 controller(s)
+ftl: error: ERROR: relation "fsm_executions" does not exist (SQLSTATE 42P01)
+
+Run again with ftl dev --recreate
. This usually indicates that your DB has an old schema.
This can occur when FTL has been upgraded with schema changes, making the database out of date. While in alpha we do not use schema migrations, so this won't occur once we hit a stable release.
+ + + + + +Glossary of terms and definitions in FTL.
+A Verb is a remotely callable function that takes an input and returns an output.
+func(context.Context, In) (Out, error)
+
+A Sink is a function that takes an input and returns nothing.
+func(context.Context, In) error
+
+A Source is a function that takes no input and returns an output.
+func(context.Context) (Out, error)
+
+An Empty function is one that takes neither input or output.
+func(context.Context) error
+
+
+
+
+
+
+ A cron job is an Empty verb that will be called on a schedule. The syntax is described here.
+You can also use a shorthand syntax for the cron job, supporting seconds (s
), minutes (m
), hours (h
), and specific days of the week (e.g. Mon
).
The following function will be called hourly:
+//ftl:cron 0 * * * *
+func Hourly(ctx context.Context) error {
+ // ...
+}
+
+Every 12 hours, starting at UTC midnight:
+//ftl:cron 12h
+func TwiceADay(ctx context.Context) error {
+ // ...
+}
+
+Every Monday at UTC midnight:
+//ftl:cron Mon
+func Mondays(ctx context.Context) error {
+ // ...
+}
+
+
+
+
+
+
+ To use an external type in your FTL module schema, declare a type alias over the external type:
+//ftl:typealias
+type FtlType external.OtherType
+
+//ftl:typealias
+type FtlType2 = external.OtherType
+
+The external type is widened to Any
in the FTL schema, and the corresponding type alias will include metadata
+for the runtime-specific type mapping:
typealias FtlType Any
+ +typemap go "github.com/external.OtherType"
+
+Users can achieve functionally equivalent behavior to using the external type directly by using the declared
+alias (FtlType
) in place of the external type in any other schema declarations (e.g. as the type of a Verb request). Direct usage of the external type in schema declarations is not supported;
+instead, the type alias must be used.
FTL will automatically serialize and deserialize the external type to the strong type indicated by the mapping.
+FTL also provides the capability to declare type mappings for other runtimes. For instance, to include a type mapping for Kotlin, you can +annotate your type alias declaration as follows:
+//ftl:typealias
+//ftl:typemap kotlin "com.external.other.OtherType"
+type FtlType external.OtherType
+
+In the FTL schema, this will appear as:
+typealias FtlType Any
+ +typemap go "github.com/external.OtherType"
+ +typemap kotlin "com.external.other.OtherType"
+
+This allows FTL to decode the type properly in other languages, for seamless +interoperability across different runtimes.
+ + + + + +FTL has first-class support for distributed finite-state machines. Each state in the state machine is a Sink, with events being values of the type of each sinks input. The FSM is declared once, with each executing instance of the FSM identified by a unique key when sending an event to it.
+Here's an example of an FSM that models a simple payment flow:
+var payment = ftl.FSM(
+ "payment",
+ ftl.Start(Invoiced),
+ ftl.Start(Paid),
+ ftl.Transition(Invoiced, Paid),
+ ftl.Transition(Invoiced, Defaulted),
+)
+
+//ftl:verb
+func SendDefaulted(ctx context.Context, in DefaultedInvoice) error {
+ return payment.Send(ctx, in.InvoiceID, in.Timeout)
+}
+
+//ftl:verb
+func Invoiced(ctx context.Context, in Invoice) error {
+ if timedOut {
+ return ftl.CallAsync(ctx, SendDefaulted, Timeout{...})
+ }
+}
+
+//ftl:verb
+func Paid(ctx context.Context, in Receipt) error { /* ... */ }
+
+//ftl:verb
+func Defaulted(ctx context.Context, in Timeout) error { /* ... */ }
+
+Then to send events to the FSM:
+err := payment.Send(ctx, invoiceID, Invoice{Amount: 110})
+
+Sending an event to an FSM is asynchronous. From the time an event is sent until the state function completes execution, the FSM is transitioning. It is invalid to send an event to an FSM that is transitioning.
+ + + + + +Verbs annotated with ftl:ingress
will be exposed via HTTP (http
is the default ingress type). These endpoints will then be available on one of our default ingress
ports (local development defaults to http://localhost:8891
).
The following will be available at http://localhost:8891/http/users/123/posts?postId=456
.
type GetRequest struct {
+ UserID string `json:"userId"`
+ PostID string `json:"postId"`
+}
+
+type GetResponse struct {
+ Message string `json:"msg"`
+}
+
+//ftl:ingress GET /http/users/{userId}/posts
+func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.HttpResponse[GetResponse, ErrorResponse], error) {
+ // ...
+}
+
+++NOTE! +The
+req
andresp
types of HTTPingress
verbs must bebuiltin.HttpRequest
andbuiltin.HttpResponse
respectively. These types provide the necessary fields for HTTPingress
(headers
,statusCode
, etc.)You will need to import
+ftl/builtin
.
Key points:
+ingress
verbs will be automatically exported by default.Given the following request verb:
+type GetRequest struct {
+ UserID string `json:"userId"`
+ Tag ftl.Option[string] `json:"tag"`
+ PostID string `json:"postId"`
+}
+
+type GetResponse struct {
+ Message string `json:"msg"`
+}
+
+//ftl:ingress http GET /users/{userId}/posts/{postId}
+func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.HttpResponse[GetResponse, string], error) {
+ return builtin.HttpResponse[GetResponse, string]{
+ Headers: map[string][]string{"Get": {"Header from FTL"}},
+ Body: ftl.Some(GetResponse{
+ Message: fmt.Sprintf("UserID: %s, PostID: %s, Tag: %s", req.Body.UserID, req.Body.PostID, req.Body.Tag.Default("none")),
+ }),
+ }, nil
+}
+
+path
, query
, and body
parameters are automatically mapped to the req
structure.
For example, this curl request will map userId
to req.Body.UserID
and postId
to req.Body.PostID
, and tag
to req.Body.Tag
:
curl -i http://localhost:8891/users/123/posts/456?tag=ftl
+
+The response here will be:
+{
+ "msg": "UserID: 123, PostID: 456, Tag: ftl"
+}
+
+Optional fields are represented by the ftl.Option
type. The Option
type is a wrapper around the actual type and can be Some
or None
. In the example above, the Tag
field is optional.
curl -i http://localhost:8891/users/123/posts/456
+
+Because the tag
query parameter is not provided, the response will be:
{
+ "msg": "UserID: 123, PostID: 456, Tag: none"
+}
+
+Field names use lowerCamelCase by default. You can override this by using the json
tag.
Given the following request verb:
+//ftl:enum export
+type SumType interface {
+ tag()
+}
+
+type A string
+
+func (A) tag() {}
+
+type B []string
+
+func (B) tag() {}
+
+//ftl:ingress http GET /typeenum
+func TypeEnum(ctx context.Context, req builtin.HttpRequest[SumType]) (builtin.HttpResponse[SumType, string], error) {
+ return builtin.HttpResponse[SumType, string]{Body: ftl.Some(req.Body)}, nil
+}
+
+The following curl request will map the SumType
name and value to the req.Body
:
curl -X GET "http://localhost:8891/typeenum" \
+ -H "Content-Type: application/json" \
+ --data '{"name": "A", "value": "sample"}'
+
+The response will be:
+{
+ "name": "A",
+ "value": "sample"
+}
+
+Complex query params can also be encoded as JSON using the @json
query parameter. For example:
+++
{"tag":"ftl"}
url-encoded is%7B%22tag%22%3A%22ftl%22%7D
curl -i http://localhost:8891/users/123/posts/456?@json=%7B%22tag%22%3A%22ftl%22%7D
+
+
+
+
+
+
+ FTL has first-class support for PubSub, modelled on the concepts of topics (where events are sent), subscriptions (a cursor over the topic), and subscribers (functions events are delivered to). Subscribers are, as you would expect, sinks. Each subscription is a cursor over the topic it is associated with. Each topic may have multiple subscriptions. Each subscription may have multiple subscribers, in which case events will be distributed among them.
+First, declare a new topic:
+var Invoices = ftl.Topic[Invoice]("invoices")
+
+Then declare each subscription on the topic:
+var _ = ftl.Subscription(Invoices, "emailInvoices")
+
+And finally define a Sink to consume from the subscription:
+//ftl:subscribe emailInvoices
+func SendInvoiceEmail(ctx context.Context, in Invoice) error {
+ // ...
+}
+
+Events can be published to a topic like so:
+Invoices.Publish(ctx, Invoice{...})
+
+++ + + + + +NOTE! +PubSub topics cannot be published to from outside the module that declared them, they can only be subscribed to. That is, if a topic is declared in module
+A
, moduleB
cannot publish to it.
Any verb called asynchronously (specifically, PubSub subscribers and FSM states), may optionally specify a basic exponential backoff retry policy via a Go comment directive. The directive has the following syntax:
+//ftl:retry [<attempts>] <min-backoff> [<max-backoff>]
+
+attempts
and max-backoff
default to unlimited if not specified.
For example, the following function will retry up to 10 times, with a delay of 5s, 10s, 20s, 40s, 60s, 60s, etc.
+//ftl:retry 10 5s 1m
+func Invoiced(ctx context.Context, in Invoice) error {
+ // ...
+}
+
+
+
+
+
+
+ Configuration values are named, typed values. They are managed by the ftl config
command-line.
To declare a configuration value use the following syntax:
+var defaultUser = ftl.Config[Username]("defaultUser")
+
+Then to retrieve a configuration value:
+username = defaultUser.Get(ctx)
+
+Secrets are encrypted, named, typed values. They are managed by the ftl secret
command-line.
Declare a secret with the following:
+var apiKey = ftl.Secret[Credentials]("apiKey")
+
+Then to retrieve a secret value:
+key = apiKey.Get(ctx)
+
+Often, raw secret/configuration values aren't directly useful. For example, raw credentials might be used to create an API client. For those situations ftl.Map()
can be used to transform a configuration or secret value into another type:
var client = ftl.Map(ftl.Secret[Credentials]("credentials"),
+ func(ctx context.Context, creds Credentials) (*api.Client, error) {
+ return api.NewClient(creds)
+})
+
+
+
+
+
+
+ Some aspects of FTL rely on a runtime which must be imported with:
+import "github.com/TBD54566975/ftl/go-runtime/ftl"
+
+
+
+
+
+
+ FTL supports the following types: Int
(64-bit), Float
(64-bit), String
, Bytes
(a byte array), Bool
, Time
, Any
(a dynamic type), Unit
(similar to "void"), arrays, maps, data structures, and constant enumerations. Each FTL type is mapped to a corresponding language-specific type. For example in Go Float
is represented as float64
, Time
is represented by time.Time
, and so on. [^1]
Any Go type supported by FTL and referenced by an FTL declaration will be automatically exposed to an FTL type.
+For example, the following verb declaration will result in Request
and Response
being automatically translated to FTL types.
type Request struct {}
+type Response struct {}
+
+//ftl:verb
+func Hello(ctx context.Context, in Request) (Response, error) {
+ // ...
+}
+
+Sum types are supported by FTL's type system, but aren't directly supported by Go. However they can be approximated with the use of sealed interfaces. To declare a sum type in FTL use the comment directive //ftl:enum
:
//ftl:enum
+type Animal interface { animal() }
+
+type Cat struct {}
+func (Cat) animal() {}
+
+type Dog struct {}
+func (Dog) animal() {}
+
+A value enum is an enumerated set of string or integer values.
+//ftl:enum
+type Colour string
+
+const (
+ Red Colour = "red"
+ Green Colour = "green"
+ Blue Colour = "blue"
+)
+
+A type alias is an alternate name for an existing type. It can be declared like so:
+//ftl:typealias
+type Alias Target
+
+or
+//ftl:typealias
+type Alias = Target
+
+eg.
+//ftl:typealias
+type UserID string
+
+//ftl:typealias
+type UserToken = string
+
+When writing a unit test, first create a context:
+func ExampleTest(t *testing.Test) {
+ ctx := ftltest.Context(
+ // options go here
+ )
+}
+
+FTL will help isolate what you want to test by restricting access to FTL features by default. You can expand what is available to test by adding options to ftltest.Context(...)
.
In this default set up, FTL does the following:
+ftl.ConfigValue
and ftl.SecretValue
(See options)ftl.Database
(See options)ftl.MapHandle
(See options)ftl.Call(...)
(See options)To enable configs and secrets from the default project file:
+ctx := ftltest.Context(
+ ftltest.WithDefaultProjectFile(),
+)
+
+Or you can specify a specific project file:
+ctx := ftltest.Context(
+ ftltest.WithProjectFile(path),
+)
+
+You can also override specific config and secret values:
+ctx := ftltest.Context(
+ ftltest.WithDefaultProjectFile(),
+
+ ftltest.WithConfig(endpoint, "test"),
+ ftltest.WithSecret(secret, "..."),
+)
+
+By default, calling Get(ctx)
on a database panics.
To enable database access in a test, you must first provide a DSN via a project file. You can then set up a test database:
+ctx := ftltest.Context(
+ ftltest.WithDefaultProjectFile(),
+ ftltest.WithDatabase(db),
+)
+
+This will:
+_test
to the database name. Eg: accounts
becomes accounts_test
By default, calling Get(ctx)
on a map handle will panic.
You can inject a fake via a map:
+ctx := ftltest.Context(
+ ftltest.WhenMap(exampleMap, func(ctx context.Context) (string, error) {
+ return "Test Value"
+ }),
+)
+
+You can also allow the use of all maps:
+ctx := ftltest.Context(
+ ftltest.WithMapsAllowed(),
+)
+
+By default, ftl.Call(...)
will fail.
You can inject fakes for verbs:
+ctx := ftltest.Context(
+ ftltest.WhenVerb(ExampleVerb, func(ctx context.Context, req Request) (Response, error) {
+ return Response{Result: "Lorem Ipsum"}, nil
+ }),
+)
+
+If there is no request or response parameters, you can use WhenSource(...)
, WhenSink(...)
, or WhenEmpty(...)
.
To enable all calls within a module:
+ctx := ftltest.Context(
+ ftltest.WithCallsAllowedWithinModule(),
+)
+
+By default, all subscribers are disabled. +To enable a subscriber:
+ctx := ftltest.Context(
+ ftltest.WithSubscriber(paymentsSubscription, ProcessPayment),
+)
+
+Or you can inject a fake subscriber:
+ctx := ftltest.Context(
+ ftltest.WithSubscriber(paymentsSubscription, func (ctx context.Context, in PaymentEvent) error {
+ return fmt.Errorf("failed payment: %v", in)
+ }),
+)
+
+Due to the asynchronous nature of pubsub, your test should wait for subscriptions to consume the published events:
+topic.Publish(ctx, Event{Name: "Test"})
+
+ftltest.WaitForSubscriptionsToComplete(ctx)
+// Event will have been consumed by now
+
+You can check what events were published to a topic:
+events := ftltest.EventsForTopic(ctx, topic)
+
+You can check what events were consumed by a subscription, and whether a subscriber returned an error:
+results := ftltest.ResultsForSubscription(ctx, subscription)
+
+If all you wanted to check was whether a subscriber returned an error, this function is simpler:
+errs := ftltest.ErrorsForSubscription(ctx, subscription)
+
+PubSub also has these different behaviours while testing:
+To declare a Verb, write a normal Go function with the following signature, annotated with the Go comment directive //ftl:verb
:
//ftl:verb
+func F(context.Context, In) (Out, error) { }
+
+eg.
+type EchoRequest struct {}
+
+type EchoResponse struct {}
+
+//ftl:verb
+func Echo(ctx context.Context, in EchoRequest) (EchoResponse, error) {
+ // ...
+}
+
+By default verbs are only visible to other verbs in the same module.
+To call a verb use ftl.Call()
. eg.
out, err := ftl.Call(ctx, echo.Echo, echo.EchoRequest{})
+
+
+
+
+
+
+ By default all declarations in FTL are visible only to the module they're declared in. The implicit visibility of types is that of the first verb or other declaration that references it.
+Exporting a declaration makes it accessible to other modules. Some declarations that are entirely local to a module, such as secrets/config, cannot be exported.
+Types that are transitively referenced by an exported declaration will be automatically exported unless they were already defined but unexported. In this case, an error will be raised and the type must be explicitly exported.
+The following table describes the directives used to export the corresponding declaration:
+Symbol | Export syntax |
---|---|
Verb | //ftl:verb export |
Data | //ftl:data export |
Enum/Sum type | //ftl:enum export |
Typealias | //ftl:typealias export |
Topic | //ftl:export 1 |
eg.
+//ftl:verb export
+func Verb(ctx context.Context, in In) (Out, error)
+
+//ftl:typealias export
+type UserID string
+
+By default, topics do not require any annotations as the declaration itself is sufficient.
+Towards a 𝝺-calculus for large-scale systems
+ Get started + +Not YAML. Declare your infrastructure in the same language you're writing in as type-safe values, rather than in separate configuration files disassociated from their point of use.
+FTL makes it possible to write backend code in your language of choice. You write normal code, and FTL extracts a service interface from your code directly, making your functions and types automatically available to all supported languages.
+There is no substitute for production data. FTL plans to support forking of production infrastructure _and_ code during development.
+Multiple versions of a single verb with different signatures can be live concurrently. See Unison for inspiration. We can statically detect changes that would violate runtime and persistent data constraints.
+We plan to integrate AI sensibly and deeply into the FTL platform. Automated AI-driven tuning suggestions, automated third-party API integration, and so on.
+