OutLayer Documentation

Persistent Storage

OutLayer provides encrypted persistent storage for your WASM projects. Data survives across executions and version updates, with automatic user isolation and atomic operations for concurrent-safe updates.

Requires Projects: Storage is only available for code deployed as a Project. You must use WASI Preview 2 (wasm32-wasip2) build target.

Overview#

All versions of a project share the same storage namespace. Data written by v1 is readable by v2. Storage is encrypted using the keystore TEE and isolated per user.

Encrypted

All data encrypted with project-specific keys in TEE

User-Isolated

Each user has their own namespace, automatic isolation

Atomic Operations

Increment, decrement, compare-and-swap for concurrency

Quick Start#

Add the OutLayer SDK to your Cargo.toml:

[dependencies]
outlayer = "0.1"
use outlayer::storage;

fn main() {
    // Store data
    storage::set("counter", &42i64.to_le_bytes()).unwrap();

    // Read data
    if let Some(data) = storage::get("counter").unwrap() {
        let value = i64::from_le_bytes(data.try_into().unwrap());
        println!("Counter: {}", value);
    }

    // Atomic increment
    let new_value = storage::increment("visits", 1).unwrap();
    println!("Visit count: {}", new_value);
}

Note: Storage automatically uses your project context. When you callrequest_execution(Project { project_id }), the contract passes the project UUID to the worker, which uses it for storage namespace and encryption.

Storage API#

The storage interface is defined in worker/wit/world.wit and imported as near:rpc/[email protected]:

interface api {
    // Basic operations
    set: func(key: string, value: list<u8>) -> string;
    get: func(key: string) -> tuple<list<u8>, string>;
    has: func(key: string) -> bool;
    delete: func(key: string) -> bool;
    list-keys: func(prefix: string) -> tuple<string, string>;

    // Conditional writes (atomic operations)
    set-if-absent: func(key: string, value: list<u8>) -> tuple<bool, string>;
    set-if-equals: func(key: string, expected: list<u8>, new-value: list<u8>) -> tuple<bool, list<u8>, string>;
    increment: func(key: string, delta: s64) -> tuple<s64, string>;
    decrement: func(key: string, delta: s64) -> tuple<s64, string>;

    // Worker storage (with public option for cross-project reads)
    // is-encrypted: true (default) = encrypted, false = plaintext (public)
    set-worker: func(key: string, value: list<u8>, is-encrypted: option<bool>) -> string;
    // project-uuid: none = current project, some("p...") = read from another project
    get-worker: func(key: string, project-uuid: option<string>) -> tuple<list<u8>, string>;

    // Version migration
    get-by-version: func(key: string, wasm-hash: string) -> tuple<list<u8>, string>;

    // Cleanup
    clear-all: func() -> string;
    clear-version: func(wasm-hash: string) -> string;
}

Methods Reference#

MethodDescriptionReturns
set(key, value)Store a key-value pairError string (empty on success)
get(key)Retrieve value by key(data, error)
has(key)Check if key existsbool
delete(key)Delete a keybool (true if existed)
list-keys(prefix)List keys with prefix(JSON array string, error)
set-if-absent(key, value)Set only if key doesn't exist(inserted: bool, error)
set-if-equals(key, expected, new)Compare-and-swap (atomic update)(success, current, error)
increment(key, delta)Atomic increment (i64)(new_value: i64, error)
decrement(key, delta)Atomic decrement (i64)(new_value: i64, error)
set-worker(key, value, is_encrypted)Store worker data (public if is_encrypted=false)Error string
get-worker(key, project_uuid)Get worker data (cross-project if project_uuid set)(data, error)

Atomic Operations#

Use atomic operations for concurrent-safe updates. These are essential for counters, rate limiters, and any state that multiple executions might modify simultaneously.

Why use atomic operations? Regular set() can cause race conditions when multiple users execute simultaneously. Atomic operations ensure data integrity.

use outlayer::storage;

// ============ set_if_absent ============
// Only inserts if key doesn't exist - perfect for initialization
if storage::set_if_absent("counter", &0i64.to_le_bytes())? {
    println!("Counter initialized to 0");
} else {
    println!("Counter already exists");
}

// ============ increment / decrement ============
// Atomic counters - handles concurrent updates automatically
let views = storage::increment("page_views", 1)?;
println!("Page views: {}", views);

let stock = storage::decrement("stock:item_123", 1)?;
if stock < 0 {
    println!("Out of stock!");
}

// ============ set_if_equals (Compare-and-Swap) ============
// Update only if current value matches expected
let current = storage::get("balance")?.unwrap_or(vec![0; 8]);
let balance = i64::from_le_bytes(current.clone().try_into().unwrap());
let new_balance = balance + 100;

match storage::set_if_equals("balance", &current, &new_balance.to_le_bytes())? {
    (true, _) => println!("Balance updated!"),
    (false, Some(actual)) => println!("Concurrent update! Retry with {:?}", actual),
    (false, None) => println!("Key was deleted"),
}

Important: increment/decrement expect values stored as 8-byte little-endian i64. If you store counters differently (e.g., as string), use set_if_equals instead.

User Data Isolation#

Storage is automatically isolated per user at the protocol level. Each user has their own namespace:

  • Automatic isolation: alice.near cannot read data stored by bob.near
  • Per-user encryption: Different encryption keys for each user's data
  • Transparent to WASM: Your code uses simple keys like balance - the platform handles namespacing
  • Caller-triggered access: WASM can only read user data when that user triggers the execution
// alice.near calls execution:
storage::set("balance", b"100");
// Database key: project_uuid:alice.near:balance = "100"

// bob.near calls execution:
storage::set("balance", b"200");
// Database key: project_uuid:bob.near:balance = "200"

// alice.near reads:
storage::get("balance")  // -> "100" (her data)

// bob.near reads:
storage::get("balance")  // -> "200" (his data)

// WASM code CANNOT read another user's data!

Worker Storage (Shared State)#

Worker-private storage uses @worker as account_id, making it shared across all users. Users cannot directly access this storage - only your WASM code can read/write it.

// Any user calls execution:
storage::set_worker("total_count", b"100");
// Database key: project_uuid:@worker:total_count = "100"

// Any other user calls:
storage::get_worker("total_count")  // -> "100" (same shared data!)

// Use case: Private Token balances
storage::set_worker("balances", balances_json);  // Shared across all users
// Users cannot directly read balances - only through your WASM methods

Use case: Worker storage is ideal for shared state like token balances, global counters, or application configuration. Users interact with this data only through your WASM methods - they cannot bypass your logic.

Public Storage (Cross-Project Reads)#

Public storage is unencrypted worker storage that can be read by other projects. This enables use cases like shared oracle price feeds, public configuration, or cross-project data sharing.

Important: Public storage is NOT encrypted. Only store data you want to be readable by other projects and external HTTP clients. Use regular worker storage for private data.

use outlayer::storage;

// Store PUBLIC data (is_encrypted = false)
storage::set_worker_with_options(
    "oracle:ETH",
    price_json.as_bytes(),
    Some(false)  // <-- Makes it public!
)?;

// Read from current project (works for both public and private)
let data = storage::get_worker("oracle:ETH")?;

// Read PUBLIC data from ANOTHER project by UUID
let price = storage::get_worker_from_project(
    "oracle:ETH",
    Some("p0000000000000001")  // Target project UUID
)?;

External HTTP API#

Public storage can also be read by external clients via HTTP:

# JSON format (default) - value is base64-encoded
curl "https://api.outlayer.fastnear.com/public/storage/get?project_uuid=p0000000000000001&key=oracle:ETH"
# {"exists":true,"value":"eyJwcmljZSI6IjM1MDAuMDAifQ=="}

# Raw format - returns raw bytes directly
curl "https://api.outlayer.fastnear.com/public/storage/get?project_uuid=p0000000000000001&key=oracle:ETH&format=raw"
# {"price":"3500.00"}

Oracle Price Feeds

Share price data across projects without API calls

Public Configuration

Share settings that other projects can read

Note: Encrypted (default) worker data is NOT accessible via cross-project reads. Only data stored with is_encrypted=false can be read by other projects.

Security#

  • All data is encrypted using keystore TEE before storage
  • Encryption key derived from: storage:{project_uuid}:{account_id}
  • Worker-private storage uses @worker as account_id
  • Data is automatically deleted when project is deleted

Warning: Deleting a project permanently removes all storage data. This action cannot be undone. Export any important data before deletion.

Use Cases#

User Preferences

Store settings, themes, language preferences per user

Counters & Analytics

Page views, API calls, usage metrics with atomic increments

Caching

Cache expensive computation results for subsequent calls

Session Data

Store temporary state between executions

Private Tokens

Use worker storage for token balances users cannot directly access

Rate Limiting

Track API calls per user with atomic counters

Oracle Price Feeds

Share public data across projects (set is_encrypted=false)

Distributed Locks

Use set_if_absent for implementing locks

Best Practices#

  • Use WASI Preview 2 (wasm32-wasip2) build target
  • Use key prefixes for organization (e.g., user:alice:settings)
  • Use increment/decrement for counters instead of get+set
  • Use set_if_absent for one-time initialization
  • Document your key format for future data migrations
  • Use get_by_version when migrating data between versions

Related Documentation