This tutorial walks you through creating and running a simple web app and a backend contract on Freenet. By the end, you’ll have:
- A web application (frontend) that runs in the browser and talks to your local Freenet node.
- A container contract that holds and distributes your web application over Freenet.
- A backend contract that stores data and provides any server-like logic.
Use the accompanying example (freenet-email-app) for reference.
1. Prerequisites
1.1 Rust & Cargo
Install Rust and Cargo (Linux/macOS example):
curl https://sh.rustup.rs -sSf | sh
Important for macOS: Using Homebrew’s Rust can interfere with
fdev
. Userustup
above instead.
1.2 Freenet Core & FDev (Git Installation)
Freenet Core is under rapid development, so install from Git:
git clone --recurse-submodules https://github.com/freenet/freenet-core.git
cd freenet-core/apps/freenet-ping
Then install the Freenet kernel (freenet
) and dev tool (fdev
):
cargo install --path ../../crates/core
cargo install --path ../../crates/fdev
1.3 WebAssembly Target
Enable the WebAssembly target for Rust:
rustup target add wasm32-unknown-unknown
1.4 Node.js & TypeScript
If you plan to build web UIs using TypeScript:
- Ubuntu:
sudo apt update sudo apt install nodejs npm
- macOS/Windows: Install from Node.js downloads.
Then install TypeScript globally:
sudo npm install -g typescript
Check:
tsc --version
2. Create Your Project Structure
We’ll create two main pieces: a web app (which is essentially our frontend) and a backend contract.
2.1 Create a Web App (Container + Frontend)
mkdir -p my-app/web
mkdir -p my-app/backend
cd my-app/web
fdev new web-app
This makes a skeleton for:
- A container contract in
./container/
(stores and distributes your web files). - A TypeScript-based web app in
./src/
.
2.2 Container Contract Overview
Open my-app/web/container/src/lib.rs
. You’ll see something like:
use freenet_stdlib::prelude::*;
struct Contract;
#[contract]
impl ContractInterface for Contract {
// ...
}
#[contract]
links your code to the Freenet WASM runtime.
Tip: Real container contracts typically add checks (e.g., who can update the contract).
3. Frontend (Web Application) Basics
Your web files live under my-app/web/src/
. A typical TypeScript setup might use npm install
to
pull in @freenetorg/freenet-stdlib
. For example in package.json
:
{
"dependencies": {
"@freenetorg/freenet-stdlib": "0.0.6"
}
}
Then in src/index.ts
, you can import @freenetorg/freenet-stdlib
and talk to the node:
import {
FreenetWsApi,
GetResponse,
HostError,
Key,
PutResponse,
UpdateResponse,
// ...
} from "@freenetorg/freenet-stdlib/websocket-interface";
const handler = {
onContractPut: (_: PutResponse) => {},
onContractGet: (_: GetResponse) => {},
onContractUpdate: (_: UpdateResponse) => {},
// ...
onErr: (err: HostError) => console.error(err),
onOpen: () => console.log("WebSocket open!"),
};
const API_URL = new URL(`ws://${location.host}/contract/command/`);
const freenetApi = new FreenetWsApi(API_URL, handler);
// Example contract key
const CONTRACT_KEY = "DCBi7HNZC3QUZRiZLFZDiEduv5KHgZfgBk8WwTiheGq1";
async function loadState() {
await freenetApi.get({
key: Key.fromSpec(CONTRACT_KEY),
fetch_contract: false,
});
}
4. Backend Contract
Switch to your backend folder and scaffold a regular contract:
cd ../backend
fdev new contract
Edit ./src/lib.rs
. Here’s a skeleton that stores a list of posts:
use freenet_stdlib::prelude::*;
struct Contract;
struct Posts(Vec<Post>);
#[derive(Serialize, Deserialize)]
struct Post {
// ...
}
#[contract]
impl ContractInterface for Contract {
fn update_state(
_params: Parameters<'static>,
current_state: State<'static>,
mut data: Vec<UpdateData<'static>>,
) -> Result<UpdateModification<'static>, ContractError> {
// Convert the current state from JSON
let mut posts: Posts = serde_json::from_slice(¤t_state)
.map_err(|_| ContractError::InvalidState)?;
if let Some(UpdateData::Delta(delta)) = data.pop() {
// Parse the incoming update as a Post
let new_post: Post = serde_json::from_slice(&delta)
.map_err(|_| ContractError::InvalidUpdate)?;
// Append to our existing posts
posts.0.push(new_post);
} else {
return Err(ContractError::InvalidUpdate);
}
// Serialize back to JSON for the updated state
Ok(UpdateModification::valid(serde_json::to_vec(&posts)
.unwrap()
.into()))
}
// ...
}
Any time the contract receives an update, it takes the JSON-encoded “post” and appends it to the stored list. The front end can subscribe to these updates and reflect them in the UI.
5. Building & Packaging
5.1 Manifest Configuration
Each contract folder has a freenet.toml
specifying how to build/package. In the web container
(my-app/web
), you might see:
[contract]
type = "webapp"
lang = "rust"
[webapp.state-sources]
source_dirs = ["dist"] # The compiled web files get packaged from this folder
You can embed your backend contract into the container by adding something like:
[webapp.dependencies]
posts = { path = "../backend" }
5.2 Compile
From inside my-app/web
:
fdev build
This runs any necessary steps (like npm install
, webpack
, TypeScript compilation) before
creating a .wasm
file and contract-state
in build/freenet
.
Then do the same in my-app/backend
:
fdev build
You should again see a .wasm
file and optionally a contract-state
.
6. Test Locally
6.1 Start the Local Freenet Node
In one terminal:
freenet
You’ll see logs indicating the node is running (HTTP gateway on 127.0.0.1:50509
by default).
6.2 Publish Contracts
In another terminal, publish both contracts:
Backend:
cd my-app/backend
fdev publish --code="./build/freenet/backend.wasm" --state="./build/freenet/contract-state"
Web Container:
cd ../web
fdev publish --code="./build/freenet/web.wasm" --state="./build/freenet/contract-state"
6.3 View in Browser
You’ll get a contract key when you publish (e.g., CYXGxQGSmcd5xHRJNQygPwmUJsWS2njh3pdVjfVz9EV
).
Open it in your browser:
http://127.0.0.1:50509/contract/web/<CONTRACT-KEY>
If everything went well, you’ll see your web application loaded from the node.
7. Updating Your App
Since the app’s code is stored as the contract’s state, you can update it at any time by:
- Changing your TypeScript/Rust source.
- Rebuilding.
- Publishing a new version with a new state or new parameters.
The Freenet node treats these as regular contract updates.
8. Current Limitations
- No Real Network: Publishing to the production Freenet network is still under development.
- Language Support: Currently only Rust for contracts; additional languages (e.g., AssemblyScript) planned.
- Manual Install: Binaries for
fdev
andfreenet
require manual build from source.
That’s it! You’ve built a basic decentralized web app on Freenet using a container contract (for the UI) and a backend contract (for the data/logic). For a more complete example, check the freenet-email-app.