This guide explains how to build FSC applications around views, sessions, identities, and platform services.
An FSC application is a Go program that embeds an FSC node, installs one or more SDKs, registers views, and starts the node runtime.
At a high level:
- Your application creates a node.
- The application installs the SDKs it needs.
- The application registers initiator and responder views.
- A client or remote party triggers a view.
- The view uses the runtime context to exchange messages, access services, and optionally interact with Fabric.
This model lets you write business protocols directly as Go code instead of pushing all coordination into chaincode or external workflow engines.
A view is the unit of application logic in FSC.
A view implements a Call() method with the general shape:
type MyView struct{}
func (v *MyView) Call(viewCtx view.Context) (any, error) {
// business logic
return nil, nil
}A view can act as:
- an initiator, which starts a protocol
- a responder, which reacts to a protocol started by another party
- a child view, executed from another view with
RunView()
Views are intentionally application-focused. They should express the steps of a business interaction: gather input, identify counterparties, exchange messages, invoke platform APIs, validate results, and return an outcome.
For more detail on the runtime that creates and executes views, see View service.
Each running view receives a view.Context, which is the runtime handle for interacting with the FSC platform.
The context gives access to:
- the local identity with
Me() - session management with
Session()andGetSession() - platform and application services with
GetService() - nested view execution with
RunView()
In practice, the context is the main object your view uses to talk to the rest of the runtime.
A session is a bidirectional communication channel between two FSC parties.
Sessions are how initiators and responders exchange protocol messages. The initiator usually opens a session to a remote identity, and the responder receives the matching session from the runtime.
Common usage pattern:
- initiator obtains a remote identity
- initiator opens a session with
GetSession() - initiator sends a message
- responder reads from
Session() - responder replies on the same session
Identities represent parties in the FSC network.
A view typically does not hardcode transport endpoints. Instead, it resolves or retrieves an identity and uses that identity when opening sessions or invoking higher-level APIs.
The example in integration/fabric/stoprestart/initiator.go uses the identity provider to resolve the party named bob, then opens a session to that identity.
FSC uses a service-oriented runtime.
Views can retrieve services from the context using GetService(). These services can be:
- FSC runtime services
- platform services from View, Fabric, or Fabric-x
- application services registered by your node
This keeps business logic decoupled from concrete wiring and storage details.
For persistence-related service boundaries, see Runtime DB access, Database layout, and Database drivers.
A typical FSC application has these pieces:
- a node entrypoint
- SDK installation
- view registration
- initiator views
- responder views
- optional application services
- optional Fabric integration
At startup, the application creates a node and installs SDKs.
A minimal node structure looks like this:
package main
import (
fscnode "github.com/hyperledger-labs/fabric-smart-client/node"
viewsdk "github.com/hyperledger-labs/fabric-smart-client/platform/view/sdk/dig"
)
func main() {
node := fscnode.New()
if err := node.InstallSDK(viewsdk.NewSDK(node)); err != nil {
panic(err)
}
node.Execute(func() error {
// Get the view registry
registry := view.GetRegistry(node)
// Register an initiator view factory
if err := registry.RegisterFactory("myInitiator", &MyInitiatorViewFactory{}); err != nil {
return err
}
// Register a responder for the initiator
initiatorID := registry.GetIdentifier(&MyInitiator{})
if err := registry.RegisterResponder(&MyResponder{}, initiatorID); err != nil {
return err
}
return nil
})
}The node runtime is provided by node.New() and started through Node.Execute().
If the application also needs Fabric capabilities, it installs the Fabric SDK in the same startup flow, as described in Fabric SDK architecture. For Fabric-X capabilities, see Fabric-X.
For node startup configuration, see View platform configuration and Shared node configuration.
The most important programming pattern in FSC is the initiator/responder pair.
An initiator view typically:
- receives input
- resolves the remote party
- opens a session
- sends one or more messages
- waits for replies
- validates the result
- returns an application outcome
From integration/fabric/stoprestart/initiator.go, the pattern is:
type Initiator struct {
in []byte
}
func (p *Initiator) Call(viewCtx view.Context) (any, error) {
identityProvider, err := id.GetProvider(viewCtx)
if err != nil {
return nil, err
}
responder := identityProvider.Identity("bob")
session, err := viewCtx.GetSession(viewCtx.Initiator(), responder)
if err != nil {
return nil, err
}
if err := session.Send(viewCtx.Context(), p.in); err != nil {
return nil, err
}
msg := <-session.Receive()
return string(msg.Payload), nil
}For a more complete example with error handling, timeouts, and message validation, see the stoprestart integration test.
This shows the essential mechanics:
- resolve a remote identity
- open a session
- send data
- receive the response
A responder view typically:
- receives the session created by the initiator
- reads the incoming message
- performs local business logic
- sends back a response or status
The responder pattern is:
type Responder struct{}
func (p *Responder) Call(viewCtx view.Context) (any, error) {
session := viewCtx.Session()
msg := <-session.Receive()
if err := session.Send(viewCtx.Context(), msg.Payload); err != nil {
return nil, err
}
return "OK", nil
}For a more complete example with error handling and timeouts, see the stoprestart integration test.
The responder does not create the session. It consumes the session associated with the incoming protocol request.
For more details on initiator/responder dispatch and responder registration, see View service.
Views are often created through factories so that FSC can instantiate them from external input.
A simple factory pattern looks like this:
type InitiatorViewFactory struct{}
func (i *InitiatorViewFactory) NewView(in []byte) (view.View, error) {
return &Initiator{in: in}, nil
}Factories are useful when:
- a view is started remotely through an API or CLI
- the runtime needs to deserialize input
- you want a clean separation between transport input and view construction
Responder views are registered against the initiator type so the runtime knows which responder to launch when a session arrives.
Conceptually, registration happens during node startup inside the callback passed to Execute().
Large business protocols are easier to maintain if they are split into smaller views.
A parent view can call another view using RunView(). This is useful when you want to separate:
- identity lookup
- validation
- data collection
- transaction assembly
- finality waiting
- persistence updates
A good rule is that each view should represent a meaningful step in a business protocol, not just a random helper function.
For reusable application logic, prefer services over duplicating wiring inside every view.
Patterns that work well:
- expose application state through a service
- retrieve the service from the context
- keep SQL and backend details behind the service boundary
- let the service use FSC persistence or Fabric APIs internally
This aligns with the service-oriented model described in view-service.md.
The View platform provides the programming model for distributed protocols. The Fabric platform adds ledger-facing capabilities on top of that model.
In practical terms:
- use the View platform to coordinate parties
- use the Fabric platform when the protocol must read state, endorse, submit, or observe transaction finality
The architecture summary in core-concepts.md describes this split:
the View platform handles orchestration, while the Fabric platform provides Fabric-aware APIs such as state, vault, transaction, and finality services.
When writing FSC applications, prefer the following style:
A view should describe a business interaction, not low-level infrastructure mechanics.
Good examples:
- collect an approval from another party
- exchange and validate transaction parameters
- submit a transaction and wait for finality
- update local application state
Less ideal examples:
- embed raw database code in every view
- spread identity resolution logic across unrelated files
- mix transport, persistence, and domain logic in a single huge
Call()
If multiple views need the same logic, move it into a service and retrieve it with GetService().
Protocols are easier to debug when the steps are clear:
- who starts
- who responds
- what message is sent
- what is validated
- what happens on timeout or error
Views should work in terms of FSC identities and business roles rather than raw addresses.
Distributed protocols fail in real systems, so views should handle:
- session creation errors
- message send and receive failures
- timeouts
- invalid or unexpected replies
- downstream Fabric errors
Here's an example of timeout handling with message validation:
ch := session.Receive()
select {
case msg := <-ch:
if msg.Status == view.ERROR {
return nil, errors.New(string(msg.Payload))
}
if string(msg.Payload) != "expected_value" {
return nil, errors.Errorf("expected expected_value, got %s", string(msg.Payload))
}
case <-time.After(1 * time.Minute):
return nil, errors.New("timeout waiting for response")
}For complete examples, see:
- pingpong - Simple ping/pong protocol with timeout and validation
- signedpingpong - Symmetric message signing using
SessionInfo.LocalPKIDandSessionInfo.RemotePKID - stoprestart - Session handling with stop/restart scenarios
- IOU - Complex Fabric integration with state management and endorsement collection
In protocols where each party must sign its message and verify the counterpart's signature, both identities — local and remote — can be resolved directly from the session without additional parameters or out-of-band lookups.
SessionInfo exposes two complementary fields for this:
RemotePKID: the public key identifier of the remote peer, cryptographically bound by the transport layer.LocalPKID: the public key identifier of the local node, the same value the remote peer sees as itsRemotePKID.
A view uses the endpoint service to map either PKID to a full view.Identity, then retrieves a signer or verifier from the sig service:
endpointSvc := endpoint.GetService(viewCtx)
sigSvc, err := sig.GetService(viewCtx)
// Resolve own identity and sign the outgoing payload.
localID, err := endpointSvc.GetIdentity("", session.Info().LocalPKID)
signer, err := sigSvc.GetSigner(localID)
signature, err := signer.Sign(payload)
// Resolve the remote identity and verify the incoming signature.
remoteID, err := endpointSvc.GetIdentity("", session.Info().RemotePKID)
verifier, err := sigSvc.GetVerifier(remoteID)
err = verifier.Verify(payload, signature)Both sides of the protocol apply the same pattern — the initiator's LocalPKID equals the responder's RemotePKID for the same session, and vice versa. This makes the logic symmetric and avoids hardcoding identity references in either view.
For a complete end-to-end example of this pattern with negative-case checks, see integration/fsc/signedpingpong.
A practical way to build an FSC application is:
- define the business protocol in terms of initiator and responder roles
- model each role as a view
- decide what data must be exchanged over sessions
- move reusable logic into services
- add Fabric integration only where ledger interaction is needed
- register views during node startup
- test the protocol end to end with integration tests