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-oracleProject names must be alphanumeric with dashes and underscores only (pattern: [a-zA-Z0-9_-]+).
Creating a Project#
Create projects via the Projects Dashboard:
- Go to /projects
- Click "New Project"
- Enter project name
- Select code source (GitHub repo or WASM URL)
- 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_uuidto the worker - Worker uses
project_uuidfor storage namespace and encryption
Project Environment Variables#
Your WASM code can access project information via environment variables:
| Variable | Example | Description |
|---|---|---|
| OUTLAYER_PROJECT_ID | alice.near/my-app | Full project ID (owner/name) |
| OUTLAYER_PROJECT_OWNER | alice.near | Project owner account |
| OUTLAYER_PROJECT_NAME | my-app | Project name (may contain "/") |
| OUTLAYER_PROJECT_UUID | 550e8400-... | 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 usedUse 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/decrementfor counters instead of get+set (race-safe) - Use
set_if_absentfor one-time initialization to avoid overwriting
Related Documentation
- Persistent Storage - Storage API, methods, atomic operations
- Project Secrets - Binding secrets to projects
- Building OutLayer App - WASI P1 vs P2, building WASM modules
- Pricing & Limits - Understanding storage costs