OutLayer Documentation

Projects

Projects allow you to organize your WASM code with version management, persistent storage, and project-level secrets. All versions of a project share the same resources, enabling seamless updates.

What are Projects?#

A Project is a container for WASM code versions with shared resources:

  • Versioning: Deploy multiple versions, switch active version anytime
  • Persistent Storage: Data survives version updates (same encryption key)
  • Project Secrets: Secrets accessible by all versions of the project
  • Storage Deposit: Pay for on-chain storage, refunded when deleted

Why use Projects? Without projects, each new WASM hash gets its own storage namespace. With projects, you can update your code while keeping all user data intact.

Project ID Format#

Each project has a unique ID composed of the owner account and project name:

{owner_account_id}/{project_name}

Examples:
  alice.near/my-app
  bob.testnet/weather-bot
  mycompany.near/trading-oracle

Project names must be alphanumeric with dashes and underscores only (pattern: [a-zA-Z0-9_-]+).

Creating a Project#

Create projects via the Projects Dashboard:

  1. Go to /projects
  2. Click "New Project"
  3. Enter project name
  4. Select code source (GitHub repo or WASM URL)
  5. Click "Create Project"

Important: Persistent storage requires WASI Preview 2 (wasm32-wasip2). Make sure to select this build target when creating your project. WASI P1 does not support storage.

How Project Binding Works#

When you execute code via a project, the contract automatically binds your WASM to the project context. You don't need to declare the project in your code — the binding is enforced by the contract.

use outlayer::storage;

fn main() {
    // Storage automatically uses your project context
    // The contract passes project_uuid to the worker
    storage::set("counter", &42i64.to_le_bytes()).unwrap();

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

How it works:

  • You call request_execution(Project { project_id })
  • Contract looks up the project and resolves the CodeSource (GitHub or WasmUrl)
  • Contract sends project_uuid to the worker
  • Worker uses project_uuid for storage namespace and encryption

Project Environment Variables#

Your WASM code can access project information via environment variables:

VariableExampleDescription
OUTLAYER_PROJECT_IDalice.near/my-appFull project ID (owner/name)
OUTLAYER_PROJECT_OWNERalice.nearProject owner account
OUTLAYER_PROJECT_NAMEmy-appProject name (may contain "/")
OUTLAYER_PROJECT_UUID550e8400-...Internal UUID (for storage)
// Access project info in your WASM code
let project_id = std::env::var("OUTLAYER_PROJECT_ID").ok();
let owner = std::env::var("OUTLAYER_PROJECT_OWNER").ok();
let name = std::env::var("OUTLAYER_PROJECT_NAME").ok();

// Example: "zavodil2.testnet/my/nested/app"
// owner = "zavodil2.testnet"
// name = "my/nested/app"  (split by first "/" only)

Security: WASM cannot fake its project — the contract determines which CodeSource runs for which project. The binding is enforced at the contract level, not in the WASM code.

Storage Security Model#

How does OutLayer ensure that a malicious WASM cannot access another project's storage?

The Security Chain

1. User calls: request_execution(Project { project_id: "alice.near/app" })

2. Contract looks up project → finds registered CodeSource (GitHub repo or WASM URL)

3. Contract sends to worker: { code_source, project_uuid: "uuid-123" }

4. Worker compiles/downloads WASM from the CodeSource

5. Worker executes WASM with storage bound to project_uuid

Key Security Properties

Contract Controls Source

Only the project owner can register CodeSources. When you request execution for alice.near/app, the contract decides which code runs — you cannot override it.

UUID from Contract

The project_uuid is generated by the contract when the project is created. WASM receives it from the worker, it cannot choose or fake its own UUID.

Deterministic WASM Hash

The WASM checksum is calculated from CodeSource: SHA256(repo:commit:target). Same source always produces the same hash — no way to substitute different code.

Storage Isolation

All storage operations are keyed by (project_uuid, account_id, key). Different projects have different UUIDs, so storage is completely isolated.

Attack Scenarios (Why They Fail)

❌ "I'll create WASM that claims to be alice.near/app"

Doesn't matter what your WASM claims. The contract looks up alice.near/app and runs whatever CodeSource Alice registered, not your code.

❌ "I'll call storage with alice's project_uuid"

You can't. The project_uuid is passed by the worker based on contract data. Your WASM only sees storage calls that are automatically scoped to your project's UUID.

❌ "I'll modify alice's project registration"

Contract enforces that only alice.near can modify projects under alice.near/*. Your account cannot change her project's CodeSource.

Bottom line: The contract is the source of truth. It maps project IDs to code sources, and workers blindly trust the contract's project_uuid. There's no way for WASM to choose which project's storage it accesses.

Managing Versions#

Each project can have multiple versions. The active version is used for new executions by default.

Adding a Version#

In the Projects dashboard, expand your project and click "Add Version". You can choose to make it active immediately or keep the current active version.

Switching Active Version#

Click the checkmark icon on any version to make it active. The currently active version cannot be removed.

Note: You cannot remove the active version. First switch to another version, then remove.

Executing a Specific Version#

By default, executions use the active version. But you can execute any specific version using the version_key parameter. This is useful for testing new versions before making them active.

// Contract call: request_execution
{
  "code_source": {
    "Project": {
      "project_id": "alice.near/my-app",
      "version_key": "zavodil/[email protected]"  // Optional: specific version
    }
  },
  "input_data": "...",
  "resource_limits": { ... }
}

// If version_key is omitted, the active version is used

Use Case: Deploy a new version, test it by specifying its version_key, and only set it as active once you confirm it works correctly. Both versions share the same storage.

Persistent Storage#

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.

Requires WASI Preview 2: Storage host functions are only available in WASI P2 components (wasm32-wasip2). WASI P1 modules do not have access to persistent storage.

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

For detailed information about storage API, methods, atomic operations, and usage examples, see the dedicated documentation:

Storage Documentation →

Project Secrets#

Secrets can be bound to a project, making them accessible by all versions. See Secrets Documentation for details on creating and managing project secrets.

Benefit: When you update your code to a new version, project secrets remain accessible. No need to re-create or migrate secrets.

Use Cases#

Hot Updates

Deploy a new version, test it via version_key, then switch active version. Rollback instantly if needed.

Data Migration

New version reads old data format using get_by_version, transforms it, writes new format.

Shared Secrets

API keys and credentials available to all versions. No need to re-enter secrets for each update.

User State Persistence

Store user preferences, counters, session data that persist across executions and version updates.

Caching

Cache expensive computation results. Subsequent executions read from cache instead of recomputing.

Worker-Private State

Use set_worker/get_worker for internal WASM state that users cannot access directly.

Atomic Counters & CAS

Use increment/decrement for concurrent-safe counters, or set_if_equals for compare-and-swap operations.

Best Practices#

  • Use WASI Preview 2 (wasm32-wasip2) build target for storage support
  • Use descriptive project names that reflect functionality
  • Tag your git commits (e.g., v1.0.0) before adding versions
  • Test new versions via version_key before setting as active
  • Keep at least one known-good version as fallback
  • Use project secrets for shared credentials instead of repo-based secrets
  • Document your storage key format for data migrations
  • Use key prefixes (e.g., user:alice:) for organization
  • Use increment/decrement for counters instead of get+set (race-safe)
  • Use set_if_absent for one-time initialization to avoid overwriting

Related Documentation