In this tutorial we will be demonstrating how web3 technology can be used to store data on-chain rather than in a database when building a landing page.

We will be using a simple Soroban smart contract to store a counter which increases each time a transaction is called. To try this at home you’ll need the following:
- Rust – https://www.rust-lang.org/
- Stellar Cli – https://developers.stellar.org/docs/build/guides/cli
- Testnet funds (free) – https://lab.stellar.org/account/create
Let’s start with the smart contract which is a modified version of the increment contract in Soroban examples.
Soroban Smart Contract
We start by importing the Soroban SDK before creating the contract iteself. The counter_key is a utility function which creates a key for each user. Then there are two public functions.
- Increment – write function which increases each wallet addresses individual counter
- Read – which returns any wallets count
#![no_std]
use soroban_sdk::{contract, contractimpl, log, symbol_short, Address, Env, Symbol};
#[contract]
pub struct MultiUserIncrementContract;
#[contractimpl]
impl MultiUserIncrementContract {
fn counter_key(user: &Address) -> (Symbol, Address) {
(symbol_short!("CTR"), user.clone())
}
pub fn increment(env: Env, caller: Address) -> u32 {
caller.require_auth();
let key = Self::counter_key(&caller);
let mut count: u32 = env.storage().instance().get(&key).unwrap_or(0);
log!(&env, "User: {}, count: {}", caller, count);
count += 1;
env.storage().instance().set(&key, &count);
env.storage().instance().extend_ttl(50, 100);
count
}
pub fn read(env: Env, user: Address) -> u32 {
let key = Self::counter_key(&user);
env.storage().instance().get(&key).unwrap_or(0)
}
}
It’s worth noting that the read function is free to call and would be executed as a simulated transaction.

This contract can be deployed to Stellar using the following commands or you can use the following contract address on testnet.
CASEN2ZFCC5MY3QQSWPFPGZZIM3VM5GSTASKBKO7AO7UCFH4NJ2PGGOA
cargo build --target wasm32-unknown-unknown --release
stellar contract deploy --wasm target/wasm32-unknown-unknown/release/multi_user_increment.wasm --source james --network testnet
Any user can interact with the contract and all on-chain data is public, which is worth noting that you can’t store private information (at least without encrypting it first).
Landing Page
The landing page is a simple index.html file which is designed to validate a product idea.
Full code is here: https://github.com/jamesbachini/Stellar-Product-Validation
We are importing the Stellar SDK which provides the logic to read and write onchain data.
<script src="https://cdnjs.cloudflare.com/ajax/libs/stellar-sdk/13.1.0/stellar-sdk.js"></script>
We can then send write transactions like this:
const rpc = new StellarSdk.rpc.Server('https://soroban-testnet.stellar.org');
const contract = new StellarSdk.Contract(CONTRACT_ID);
const networkPassphrase = StellarSdk.Networks.TESTNET;
const sourceKeypair = StellarSdk.Keypair.fromSecret(SECRET_KEY);
const scValAddress = StellarSdk.nativeToScVal(PUBLIC_KEY, { type: "address" });
const sourceAccount = await rpc.getAccount(PUBLIC_KEY);
const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
}).addOperation(contract.call("increment", scValAddress)).setTimeout(30).build();
const preparedTx = await rpc.prepareTransaction(tx);
preparedTx.sign(sourceKeypair);
const txResult = await rpc.sendTransaction(preparedTx);
console.log('txResult', txResult);
And read transactions are simulated like this:
const tx = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(contract.call("read", scValAddress))
.setTimeout(30)
.build();
rpc.simulateTransaction(tx).then((sim) => {
const decoded = StellarSdk.scValToNative(sim.result?.retval);
alert(`Clicks: ${decoded}`);
});
The final product is a simple web page that stores information on a decentralized p2p network rather than a database.