Build an Offline-first App
This guide shows how to build a small offline-first application with Softadastra.
The goal is simple: write data locally, read it locally, and keep the application useful even when no network, server, peer, transport, or discovery is available.
The core rule is:
Local work must not depend on network availability.What you will build
You will build a small local-first app that:
- creates a Softadastra client
- opens the local runtime
- writes a value locally
- reads the value locally
- prints the result
- checks local store size
- closes the runtime
The flow is:
ClientOptions
↓
Client
↓
open
↓
put
↓
get
↓
closeThis is the smallest useful Softadastra workflow.
Why this guide starts offline
Softadastra is designed for real-world environments where the network can fail.
A user action should not automatically fail just because:
- internet is unavailable
- server is unreachable
- peer is offline
- transport is disabled
- discovery found no peers
- sync is delayed
So the first application should prove one thing:
local write
↓
local state
↓
local readNo network is required.
Choose your SDK
You can build this first app with either SDK:
C++ SDK -> native C++ application
JS SDK -> JavaScript or Node.js applicationBoth follow the same model.
C++:
client.put("app/name", "Softadastra SDK");JavaScript:
await client.put("app/name", "Softadastra SDK");The API shape is different, but the idea is the same: write locally first.
Option A: C++ offline-first app
Use this version if you want to build with the C++ SDK.
Create the file
mkdir softadastra-cpp-offline-app
cd softadastra-cpp-offline-app
nano main.cppPaste this code:
#include <iostream>
#include <softadastra/sdk.hpp>
int main()
{
using namespace softadastra::sdk;
ClientOptions options =
ClientOptions::local("node-offline");
options.enable_transport = false;
options.enable_discovery = false;
options.enable_wal = false;
Client client{options};
auto open_result = client.open();
if (open_result.is_err())
{
std::cerr << "failed to open client: "
<< open_result.error().message()
<< "\n";
return 1;
}
auto put_result = client.put(
"app/name",
"Softadastra SDK");
if (put_result.is_err())
{
std::cerr << "failed to write value: "
<< put_result.error().message()
<< "\n";
client.close();
return 1;
}
auto value_result = client.get("app/name");
if (value_result.is_err())
{
std::cerr << "failed to read value: "
<< value_result.error().message()
<< "\n";
client.close();
return 1;
}
std::cout << "offline-first app\n";
std::cout << " key : app/name\n";
std::cout << " value : "
<< value_result.value().to_string()
<< "\n";
std::cout << " size : "
<< client.size()
<< "\n";
client.close();
return 0;
}What this configuration means
ClientOptions options =
ClientOptions::local("node-offline");This creates a local Softadastra node named node-offline.
options.enable_transport = false;
options.enable_discovery = false;
options.enable_wal = false;For the first app, transport, discovery, and WAL are disabled.
That means:
transport disabled -> no peer connection required
discovery disabled -> no peer discovery required
WAL disabled -> memory-only local stateThe application is still local-first.
It can write and read local data.
Expected output
offline-first app
key : app/name
value : Softadastra SDK
size : 1Option B: JavaScript offline-first app
Use this version if you want to build with the JavaScript SDK.
Create the project
mkdir softadastra-js-offline-app
cd softadastra-js-offline-app
npm init -y
npm pkg set type=module
npm install @softadastra/sdk
nano main.jsPaste this code:
import { Client, ClientOptions } from "@softadastra/sdk";
const options = ClientOptions.local("node-offline");
options.enableTransport = false;
options.enableDiscovery = false;
options.enableWal = false;
const client = new Client(options);
const openResult = await client.open();
if (openResult.isErr()) {
console.error(`failed to open client: ${openResult.error().message}`);
process.exit(1);
}
const putResult = await client.put(
"app/name",
"Softadastra SDK",
);
if (putResult.isErr()) {
console.error(`failed to write value: ${putResult.error().message}`);
await client.close();
process.exit(1);
}
const valueResult = await client.get("app/name");
if (valueResult.isErr()) {
console.error(`failed to read value: ${valueResult.error().message}`);
await client.close();
process.exit(1);
}
console.log("offline-first app");
console.log(" key : app/name");
console.log(` value : ${valueResult.value().toString()}`);
console.log(` size : ${client.size()}`);
await client.close();Run the app
node main.jsExpected output:
offline-first app
key : app/name
value : Softadastra SDK
size : 1What happened internally
Even in the simplest app, the Softadastra model is visible.
application
↓
client.open()
↓
client.put()
↓
local store
↓
client.get()
↓
client.close()With WAL disabled, the flow is memory-only:
put
↓
validate key and value
↓
apply to local store
↓
return resultThere is no network step.
There is no peer step.
There is no server step.
That is the point of the first offline-first app.
Why transport is disabled
Transport is the delivery layer.
It connects to peers and moves messages.
For this first app, transport is disabled because the goal is not peer communication yet.
local store works first
transport can be added laterA local application should not need transport to write local data.
Why discovery is disabled
Discovery finds peers.
For this first app, discovery is disabled because the app does not need to find another node.
discovery finds peers
transport connects peers
sync sends operationsThose steps come later.
The local store can work without them.
Why WAL is disabled
WAL gives durability.
For the first app, WAL is disabled to keep the example minimal.
That means local state is memory-only:
process running -> value exists
process exits -> value may be lostThis is useful for learning, tests, demos, and temporary state.
When you need recovery after restart, use persistent store.
Add a second value
You can add more local values.
C++:
client.put("settings/theme", "dark");
client.put("profile/name", "Ada");JavaScript:
await client.put("settings/theme", "dark");
await client.put("profile/name", "Ada");Then the store size should increase.
size : 3Read a missing value
A missing key should return an explicit error.
C++:
auto result = client.get("missing/key");
if (result.is_err())
{
std::cout << result.error().code_string() << "\n";
}JavaScript:
const result = await client.get("missing/key");
if (result.isErr()) {
console.log(result.error().codeString());
}Expected output style:
not_foundA missing key is a normal local store error. It should not crash the app.
Remove a value
C++:
auto removed = client.remove("app/name");
if (removed.is_err())
{
std::cerr << removed.error().message() << "\n";
}JavaScript:
const removed = await client.remove("app/name");
if (removed.isErr()) {
console.error(removed.error().message);
}After removal, reading the same key should return not_found.
Inspect local state with the CLI
You can test the same idea from the CLI.
softadastra store put app/name Softadastra
softadastra store get app/name
softadastra store remove app/nameThe CLI follows the same local-first model:
store put
↓
local write
↓
local stateIt should not require a peer or remote server.
What this app guarantees
This first app demonstrates these guarantees:
- local write can happen without network
- local read can happen without network
- transport is not required
- discovery is not required
- peers are not required
- server access is not required
- errors are explicit
This is the foundation of Softadastra.
What this app does not guarantee
This first app does not provide persistence after restart because WAL is disabled.
It does not synchronize with other nodes because transport and discovery are disabled.
It does not resolve conflicts because there is only one local node.
It does not prove convergence because no remote operation is exchanged.
Those features are added step by step in later guides.
Common mistakes
Expecting data to survive restart
In this first app, WAL is disabled.
enable_wal = false
enableWal = falseSo the state is memory-only.
Use persistent store when data must survive restart.
Expecting sync to complete automatically
This app does not start transport, discovery, or a peer connection.
A local write can create sync work in more advanced configurations, but synchronization is a separate phase.
Ignoring result values
Do not call value() before checking success.
C++:
auto result = client.get("app/name");
if (result.is_ok())
{
std::cout << result.value().to_string() << "\n";
}JavaScript:
const result = await client.get("app/name");
if (result.isOk()) {
console.log(result.value().toString());
}Recommended next experiment
Change the value.
C++:
client.put("app/name", "Softadastra Offline App");JavaScript:
await client.put("app/name", "Softadastra Offline App");Then read it again.
The result should show the new value.
This proves that the local store holds the current state.
Summary
You built the smallest Softadastra offline-first app.
The key model is:
open local runtime
↓
write locally
↓
read locally
↓
close runtimeThe network was not required.
The next step is to run a local node and inspect the runtime from the CLI.
Next step
Continue with: