Build a local-first app
A local-first app gets its blockchain data pushed to it. You embed a DefraDB instance inside your Go app with the app-sdk, subscribe to a View, and Host clients push the View's pre-processed data to your app over P2P. After that, queries run against your local copy: no per-query API calls, and the data is verifiable because every document carries signatures from the Generator clients that wrote it. For the reasoning behind this model, see The Shinzo app model.
By the end of this tutorial you have a running Go program that subscribes to a View hosted on the public testnet and answers queries locally. You do not need a wallet, and you do not run a Generator or Host client yourself. The only setup is the Go toolchain.
Pushed replication is currently blocked by a version mismatch. The app-sdk pins DefraDB v0.20 while public Host clients run DefraDB v1.0, and documents published by a v1.0 Host cannot be parsed by a v0.20 app, so nothing arrives yet. Every step below is correct against the current app-sdk: your app connects and subscribes successfully, and its queries return empty until the SDK ships a compatible DefraDB. If you need data in a Go app today, query a Host directly instead, as described in Query data.
Before you start
- Go 1.25 or later.
- A C compiler such as gcc. DefraDB pulls in a native module, so builds need CGO enabled. On macOS and most Linux machines with a standard Go install this works out of the box.
- About 15 minutes.
Scaffold the app
-
Create a new module and pull the app-sdk:
mkdir shinzo-app cd shinzo-app go mod init shinzo-app go get github.com/shinzonetwork/shinzo-app-sdkNotego get github.com/shinzonetwork/app-sdkfails with a module path error. The repository was renamed and the module now declaresgithub.com/shinzonetwork/shinzo-app-sdk. -
Create
main.gowith the imports the rest of the tutorial uses:package main import ( "context" "strings" "github.com/shinzonetwork/shinzo-app-sdk/pkg/config" "github.com/shinzonetwork/shinzo-app-sdk/pkg/defra" "github.com/shinzonetwork/shinzo-app-sdk/pkg/views" )
Configure the app
The app-sdk loads a config.yaml at startup. Create one with these contents:
defradb:
url: "http://localhost:9181"
keyring_secret: "dev-secret"
p2p:
enabled: true
bootstrap_peers: []
listen_addr: "/ip4/127.0.0.1/tcp/9171"
store:
path: "./.defra"
shinzo:
minimum_attestations: 1
logger:
development: true
A few keys matter more than the rest:
defradb.keyring_secretis required. It encrypts the local keyring that holds your node's identity, so your app keeps the same P2P identity across restarts. You can also set it through theDEFRA_KEYRING_SECRETenvironment variable instead of the file.defradb.p2p.enabledmust betrue. Without it the SDK starts with networking off and no data can be pushed to you.shinzo.minimum_attestationssets the default attestation threshold used when filtering queries. The tutorial does not use attestation filtering, so this only needs a syntactically valid value.logger.developmentkeeps DefraDB's logs visible while you learn. Set it tofalsein production.
Connect to a Host
Your embedded DefraDB instance discovers Host clients by dialing bootstrap peers. Registered Host clients publish their connection strings on-chain, and the testnet registry exposes them over REST.
-
List the registered Host clients:
curl -s http://testnet.shinzo.network:1317/shinzonetwork/host/v1/hosts | jq -r '.hosts[].connection_string'/ip4/34.63.186.249/tcp/9171/p2p/12D3KooWSqvLctTtcQLvqSVZU4sTCUWxCX9z4NeFpSHnmVWBiFMZ /ip4/65.109.106.214/tcp/9171/p2p/12D3KooWCZgmwi1Kz6Sjqkpm4b8b4D5Hvb82KwyFPpGRZPuhFENB ... -
Pick one or two connection strings and add them to
bootstrap_peersin yourconfig.yaml:defradb: p2p: enabled: true bootstrap_peers: - "/ip4/34.63.186.249/tcp/9171/p2p/12D3KooWSqvLctTtcQLvqSVZU4sTCUWxCX9z4NeFpSHnmVWBiFMZ" listen_addr: "/ip4/127.0.0.1/tcp/9171"
The registry moves over time, so if dialing fails, pull the list again and swap in a current Host. For more ways to find Hosts and what each field in the registry means, see Find Views and Hosts and Connect your app to a Host.
Running your own Generator client and Host client locally for development? Point bootstrap_peers at your Host instead, for example /ip4/127.0.0.1/tcp/9171/p2p/<your-host-peer-id>.
Start the embedded DefraDB instance
Add the startup code to main.go:
-
Load the config you wrote:
shinzoConfig, err := config.LoadConfig("config.yaml") if err != nil { panic(err) } -
Start DefraDB and close it when the program exits:
myNode, _, err := defra.StartDefraInstance( shinzoConfig, &defra.MockSchemaApplierThatSucceeds{}, nil, nil, ) if err != nil { panic(err) } defer myNode.Close(context.Background())MockSchemaApplierThatSucceedsis the schema applier to use when DefraDB only holds Shinzo data. If your app also stores its own documents in DefraDB, useSchemaApplierFromFileorSchemaApplierFromProvidedSchemainstead, and put your schema there. The twonilarguments are optional node options and a replication filter, which this tutorial does not need, and the second return value is the network handler, which you can ignore here.
Subscribe to the View
Subscribing does two things: it applies the View's SDL to your embedded DefraDB instance so the collection exists locally, and it registers that collection with Defra's passive replication so Host clients know to push its documents to you.
The tutorial uses Studio_v1_Erc20TransferUSDC, a View already registered on the public testnet. It decodes Transfer events from a token contract and exposes them as documents with token address, sender, recipient, amount, and block number. If you already finished Create your first View, you can substitute that View instead.
-
Define the View and subscribe to it:
sdl := `type Studio_v1_Erc20TransferUSDC { tokenAddress: String hash: String blockNumber: Int from: String to: String amount: String }` view := views.View{ Name: "Studio_v1_Erc20TransferUSDC", Sdl: &sdl, } err = view.SubscribeTo(context.Background(), myNode) if err != nil { if strings.Contains(err.Error(), "collection already exists") { // You have subscribed before. The error is informational and safe to ignore. } else { panic(err) } }NoteThe View's SDL as registered on the hub includes the directive
@materialized(if: false), which tells the Host client to compute results on query instead of storing them. The DefraDB version the app-sdk embeds rejects that directive, so the app applies the SDL without it and stores pushed documents locally. Only theName,Sdl, and optionallyQueryfields of the View struct matter for subscribing.
Query your data
Host clients push the View's documents into your local collection as they process new blocks. Once subscribed, you query the collection with the SDK's helper and a Go struct that matches the fields.
-
Describe the result type:
type Transfer struct { TokenAddress string `json:"tokenAddress"` From string `json:"from"` To string `json:"to"` Amount string `json:"amount"` BlockNumber int `json:"blockNumber"` } -
Query with
defra.QueryArray:transfers, err := defra.QueryArray[Transfer]( context.Background(), myNode, `query { Studio_v1_Erc20TransferUSDC(limit: 10) { tokenAddress from to amount blockNumber } }`, ) if err != nil { panic(err) }
Until pushed replication works across the DefraDB version gap described at the top, this returns an empty slice. Once a compatible app-sdk release lands, you will see the slice fill with transfers a few moments after subscribing, and the same program keeps receiving new data as long as the Host clients keep running. For richer querying, defra.QuerySingle fetches one document. For all the GraphQL filters and ordering you can use here, see Query data.
That is the whole local-first flow: subscribe once, then treat the embedded DefraDB instance as your read model.
Where to next
- Create your first View to define your own data instead of using a public View.
- Subscribe to Views with the app-sdk for the full configuration surface, including schema appliers and clean shutdown.
- The Shinzo app model for the concepts behind pushed data and local querying.
- Choosing an app architecture to compare this with direct signed queries and running your own Host.
Need help
- For onboarding and technical support, join the Shinzo Discord.
- To report a documentation bug or request a feature, open an issue in the docs repo.
- For a technical issue with the app-sdk client, open an issue in the app-sdk repo.