Encrypted Data Storage with Nillion Blind Computing
Store encrypted data permanently on Arweave while controlling access through Nillion's nilDB private storage. This approach ensures your data remains encrypted on Arweave, decryption keys are securely stored in nilDB (Private Storage), and only authorized users can access and decrypt the data.
Arweave provides permanent, immutable storage for encrypted data, while Nillion's nilDB stores decryption keys encrypted and split across multiple nodes. Users can encrypt files using their private keys stored in nilDB, upload the encrypted data to Arweave, and later retrieve and decrypt the data using the same private key—all while maintaining complete control over access permissions.
Prerequisites
Before starting, you'll need to complete Nillion's setup process:
Create Builder and Get Tokens
Create a Test Builder:
- Visit Nillion Subscription Portal
- Create a testnet public/private key pair for network access
- Use two distinct keys: one for network access, one for subscription payments
Get Testnet NIL Tokens:
- Visit NIL Faucet
- Fund your account with testnet NIL tokens
Subscribe to nilDB Service
Activate nilDB Subscription:
- Use your subscription wallet to pay for nilDB service
- Save your private key in hex format for authentication
- You'll need this key to access Nillion's Private Storage services
Set Up Development Environment
Create a New Project:
mkdir nillion-arweave-demo
cd nillion-arweave-demo
npm init -yInstall Required Dependencies:
npm install @nillion/secretvaults @nillion/nuc @nillion/blindfold @ardrive/turbo-sdk arweave dotenv @noble/curves consola
npm install --save-dev typescript @types/node ts-node @tsconfig/node20Set Up TypeScript:
npx tsc --initUpdate tsconfig.json:
{
"extends": "@tsconfig/node20/tsconfig.json",
"compilerOptions": {
"outDir": "dist/src",
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"allowUnusedLabels": false,
"allowImportingTsExtensions": true,
"noEmit": true,
"allowUnreachableCode": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src"],
"exclude": ["dist", "bundle", "node_modules"]
}Configure Environment Variables:
Create a .env file with your configuration:
NIL_BUILDER_PRIVATE_KEY=your_hex_private_key_here
NIL_BUILDER_COLLECTION_ID=your_collection_id_here # If it exists, one will be created otherwise
NILCHAIN_URL=http://rpc.testnet.nilchain-rpc-proxy.nilogy.xyz
NILAUTH_URL=https://nilauth.sandbox.app-cluster.sandbox.nilogy.xyz
NILDB_NODES=https://nildb-stg-n1.nillion.network,https://nildb-stg-n2.nillion.network,https://nildb-stg-n3.nillion.networkUnderstanding the Implementation
Let's build the application step by step, explaining what each part does:
Set Up Project Structure and Imports
First, create the main application file src/index.ts and understand the imports:
import { SecretVaultBuilderClient, SecretVaultUserClient, Uuid } from "@nillion/secretvaults";
import { createWallet, downloadFile, uploadFile } from "./arweave";
import { JWKInterface } from "arweave/node/lib/wallet";
import { logger } from "./logger";
import { Keypair, Command, NucTokenBuilder, Did, NucTokenEnvelope, DelegationBody, InvocationBody } from "@nillion/nuc";
import fs from "fs";
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
import { randomUUID } from "node:crypto";
import { SecretKey } from "@nillion/blindfold";
import { bytesToHex } from "@noble/curves/utils";
import "dotenv/config";What these imports do:
- Nillion SDKs:
@nillion/secretvaultsand@nillion/nucfor private storage and access control - Arweave:
arweaveand@ardrive/turbo-sdkfor permanent data storage - Crypto: Node.js built-in crypto functions for AES-256-GCM encryption
- Utilities: Helper functions for key conversion and UUID generation
Define Configuration and Types
Set up the application configuration with proper TypeScript types:
export type AppConfig = {
NILCHAIN_URL: string;
NILAUTH_URL: string;
NILDB_NODES: string[];
NIL_PAYER_PRIVATE_KEY: string;
NIL_BUILDER_PRIVATE_KEY: string;
NIL_BUILDER_COLLECTION_ID: string;
};
const config: AppConfig = {
NILCHAIN_URL: process.env.NILCHAIN_URL || "http://rpc.testnet.nilchain-rpc-proxy.nilogy.xyz",
NILAUTH_URL: process.env.NILAUTH_URL || "https://nilauth.sandbox.app-cluster.sandbox.nilogy.xyz",
NILDB_NODES: process.env.NILDB_NODES ? process.env.NILDB_NODES.split(",") : [
"https://nildb-stg-n1.nillion.network",
"https://nildb-stg-n2.nillion.network",
"https://nildb-stg-n3.nillion.network",
],
NIL_BUILDER_PRIVATE_KEY: process.env.NIL_BUILDER_PRIVATE_KEY,
NIL_BUILDER_COLLECTION_ID: process.env.NIL_BUILDER_COLLECTION_ID,
};What this does:
- Type safety: Defines the structure of our configuration object
- Environment variables: Loads settings from
.envfile with fallback defaults - Network endpoints: Points to Nillion testnet infrastructure
- Multiple nodes: Uses 3 nilDB nodes for redundancy and security
Create Encryption and Decryption Functions
Implement AES-256-GCM encryption using the user's private key:
// Utility functions
const downloadFile = async (url, location) => {
try {
const response = await fetch(url);
const data = await response.text();
await fs.promises.writeFile(location, data);
console.log(`💾 Downloaded file saved to: ${location}`);
} catch (error) {
console.error("❌ Download file failed:", error);
}
};
const encryptContent = (content, encryptionKey) => {
// Convert hex private key to buffer
const privateKeyBuffer = Buffer.from(encryptionKey, "hex");
// Derive a 32-byte encryption key from your private key using SHA-256
const derivedKey = createHash("sha256").update(privateKeyBuffer).digest();
// Validate derived key length (should be 32 bytes for AES-256)
if (derivedKey.length !== 32) {
throw new Error("Derived encryption key must be exactly 32 bytes for AES-256");
}
// Generate secure encryption parameters
const iv = randomBytes(16);
// Encrypt using AES-256-GCM
const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
let encrypted = cipher.update(content);
encrypted = Buffer.concat([encrypted, cipher.final()]);
// Get the authentication tag (16 bytes for GCM)
const authTag = cipher.getAuthTag();
// Combine IV + authTag + encrypted data into a single buffer
return Buffer.concat([iv, authTag, encrypted]);
};What this does:
- Key derivation: Uses SHA-256 to create a 32-byte encryption key from the user's private key
- AES-256-GCM: Provides both confidentiality and authenticity
- Random IV: Each encryption uses a unique initialization vector
- Auth tag: Prevents tampering with the encrypted data
- Combined format: Stores IV, auth tag, and encrypted data together
Create Decryption Function
Implement the corresponding decryption function:
const decryptContent = (encryptedData, encryptionKey, outputLocation) => {
// Convert hex private key to buffer
const privateKeyBuffer = Buffer.from(encryptionKey, "hex");
// Derive the same 32-byte encryption key using SHA-256
const derivedKey = createHash("sha256").update(privateKeyBuffer).digest();
// Validate derived key length
if (derivedKey.length !== 32) {
throw new Error("Derived encryption key must be exactly 32 bytes for AES-256");
}
// Extract IV, auth tag, and encrypted content
if (encryptedData.length < 32) {
throw new Error("Invalid encrypted data: too short");
}
const iv = encryptedData.subarray(0, 16);
const authTag = encryptedData.subarray(16, 32);
const encrypted = encryptedData.subarray(32);
// Create decipher
const decipher = createDecipheriv("aes-256-gcm", derivedKey, iv);
// Set the authentication tag
decipher.setAuthTag(authTag);
// Decrypt the data
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
console.log("✅ Successfully decrypted!");
// Save decrypted file
fs.writeFileSync(outputLocation, decrypted);
console.log(`💾 Decrypted file saved to: ${outputLocation}`);
return decrypted;
};What this does:
- Recreates the key: Uses the same SHA-256 derivation process
- Extracts components: Separates IV, auth tag, and encrypted data
- Validates integrity: The auth tag ensures data hasn't been tampered with
- Decrypts safely: Only works with the correct private key
- Saves result: Writes the decrypted data to a file
Create Token Generation Function
Implement delegation token creation for secure access control:
const generateToken = async (parentToken, command, audience, tokenExpirySeconds, privateKey, body) => {
const token = NucTokenBuilder.extending(parentToken)
.command(command)
.audience(audience)
.expiresAt(Math.floor(Date.now() / 1000) + tokenExpirySeconds);
if (body) token.body(body);
return token.build(privateKey);
};What this does:
- Delegation tokens: Creates time-limited access tokens for users
- Command-based: Specifies exactly what operations the token allows
- Audience-specific: Only works for the intended user
- Time-limited: Automatically expires after the specified duration
- Cryptographically signed: Uses the builder's private key for authenticity
Create Utility Files
Create supporting utility files for Arweave operations and logging:
Create src/arweave.ts:
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import { logger } from './logger';
import { TurboFactory, ArweaveSigner } from "@ardrive/turbo-sdk";
const ARWEAVE_HOST = "arweave.net";
export async function createWallet() {
try {
const arweave = Arweave.init({
host: ARWEAVE_HOST,
port: 1984,
protocol: 'http',
timeout: 20000,
logging: true,
});
// Generate a new wallet
const wallet = await arweave.wallets.generate();
// Get the wallet address
const address = await arweave.wallets.jwkToAddress(wallet);
return { wallet, address };
} catch (error) {
logger.error('Error creating wallet:', error);
}
}
export const uploadFile = async (data: string, wallet: JWKInterface) => {
try {
const signer = new ArweaveSigner(wallet);
const turbo = TurboFactory.authenticated({ signer });
const result = await turbo.upload({
data,
dataItemOpts: {
tags: [
{ name: "Content-Type", value: "text/plain" },
{ name: "Title", value: "My demo file" },
],
},
});
return result;
} catch (error) {
logger.error('Error uploading securely encrypted file:', error);
}
}Create src/logger.ts:
import { createConsola } from 'consola'
export const logger = createConsola({})What these do:
- Arweave utilities: Handle wallet creation and file uploads using Turbo
- Consola logging: Provides a beautiful, consistent logging experience with colors and formatting
- Error handling: Gracefully handle and log errors
Implement the Main Workflow
Now let's implement the main application logic that ties everything together:
async function main() {
try {
let collectionId;
const dataId = randomUUID();
// Builder Client
const builderKeypair = Keypair.from(config.NIL_BUILDER_PRIVATE_KEY);
const builder = await SecretVaultBuilderClient.from({
keypair: builderKeypair,
urls: {
chain: config.NILCHAIN_URL,
auth: config.NILAUTH_URL,
dbs: config.NILDB_NODES,
},
blindfold: { operation: "store" },
});
await builder.refreshRootToken();
// Check if builder is set up correctly
const existingProfile = await builder.readProfile();What this does:
- Builder initialization: Creates a builder client with your private key
- Network connection: Connects to Nillion testnet infrastructure
- Token refresh: Gets a fresh authentication token
- Profile validation: Ensures the builder is properly set up
Set Up Data Collection
Create or validate the collection for storing user private keys:
// Create the Owned Collection if it doesn't exist
if (!config.NIL_BUILDER_COLLECTION_ID) {
collectionId = randomUUID();
const schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
type: "array",
items: {
type: "object",
properties: {
_id: { "type": "string", "format": "uuid" },
private_key: {
"type": "object",
"properties": { "%share": { "type": "string" } },
required: ["%share"]
}
},
required: ["_id", "private_key"]
}
};
const collection = {
_id: collectionId,
type: "owned",
name: "Nillion / Arweave Demo App User Profiles",
schema,
};
const collectionResult = await builder.createCollection(collection);
console.log(`✅ Created Owned Collection with ID: ${collectionId}`);
console.log(`Make sure to update your .env file with this NIL_BUILDER_COLLECTION_ID to proceed.`);
process.exit(0);
} else {
if (existingProfile.data.collections.indexOf(config.NIL_BUILDER_COLLECTION_ID) === -1) {
throw new Error(`Builder does not have collection ${config.NIL_BUILDER_COLLECTION_ID} registered. Please check your .env configuration.`);
} else {
console.log(`✅ Builder is set up correctly with DID: ${builder.did}`);
console.log(`✅ Using Owned Collection ID: ${config.NIL_BUILDER_COLLECTION_ID}`);
collectionId = config.NIL_BUILDER_COLLECTION_ID;
}
}What this does:
- Collection creation: Creates a new collection if none exists
- Schema definition: Defines the structure for storing user private keys
- Validation: Ensures the builder has access to the specified collection
- Environment setup: Prompts you to update your
.envfile with the collection ID
Create User and Store Private Key
Generate a new user and securely store their private key in nilDB:
// Create user and store its private key in nilDB
const secretKey = await SecretKey.generate(
{ nodes: config.NILDB_NODES.map(url => ({ url })) },
{ store: true }
);
const userKeypair = Keypair.from(bytesToHex(secretKey.material));
const userDid = userKeypair.toDid().toString();
const user = await SecretVaultUserClient.from({
baseUrls: config.NILDB_NODES,
keypair: userKeypair,
blindfold: { operation: "store" }
});
// Grant write access to the user
const delegationToken = await generateToken(
builder.rootToken,
new Command(["nil", "db", "data", "create"]),
userKeypair.toDid(),
3600, // 1 hour
builder.keypair.privateKey()
);
console.log(`🗝️ Delegation token created`);
// User creates profile with Private Key
await user.createData(delegationToken, {
owner: userDid,
acl: {
grantee: builder.did.toString(),
read: false,
write: false,
execute: true,
},
collection: collectionId,
data: [
{
_id: dataId,
private_key: { "%allot": userKeypair.privateKey() },
}
],
});
console.log(`✅ User profile created: ${userDid}`);What this does:
- Secret key generation: Creates a cryptographically secure private key
- Keypair creation: Converts the secret key to a usable keypair
- User client: Initializes a user client for nilDB operations
- Delegation token: Grants the user permission to create data
- ACL setup: Defines access control - builder can execute queries but not read/write
- Data storage: Stores the user's private key in the nilDB collection
Encrypt and Upload File
Now let's encrypt a file and upload it to Arweave:
const wallet = await createWallet();
console.log(`💼 Arweave wallet created: ${wallet?.address}`);
// Retrieve the user private key from nilDB
const retrievedUserKey = await user.readData({
collection: collectionId,
document: dataId,
});
// Encrypt file contents
const fileData = fs.readFileSync("test/demo.txt");
const encrypted = encryptContent(fileData, retrievedUserKey.data.private_key);
const upload = await uploadFile(encrypted, wallet?.wallet);
console.log(`✅ File uploaded to Arweave with txId: ${upload.id}`);
// Download the file and decrypt it
const downloadFileName = `./test/encrypted_demo_${Date.now()}.txt`;
console.log(`🕒 Downloading file from Arweave`);
await downloadFile(`https://arweave.net/${upload.id}`, downloadFileName);
const decrypted = decryptContent(encrypted, retrievedUserKey.data.private_key, `./test/decrypted_demo_${Date.now()}.txt`);What this does:
- Arweave wallet: Creates a new wallet for uploading files
- Key retrieval: Gets the user's private key from nilDB
- File encryption: Encrypts the file using the user's private key
- Arweave upload: Uploads the encrypted file to permanent storage
- File download: Downloads the file from Arweave
- Decryption: Decrypts the file using the same private key
Complete the Application
Finish the main function with error handling:
} catch (error) {
console.log(error);
console.error("⚠️ Error setting up builder:", JSON.stringify(error, null, 2));
}
}
main().catch(console.error);What this does:
- Error handling: Catches and logs any errors that occur during execution
- Application startup: Runs the main function when the script is executed
Create Test File
Before running the demo, create a test file to encrypt:
mkdir test
echo "Hello, this is a test file for Nillion + Arweave encryption demo!" > test/demo.txtUnderstanding the Complete Workflow
The main application performs the following steps:
- Initialize Builder: Sets up the Nillion Builder client with your private key
- Create/Verify Collection: Creates a new collection or uses an existing one for storing private keys
- Generate User: Creates a new user with a private key stored securely in nilDB
- Create Wallet: Generates an Arweave wallet for uploading data
- Encrypt Data: Encrypts the test file using the user's private key
- Upload to Arweave: Uploads the encrypted data to Arweave using Turbo
- Download & Decrypt: Downloads the encrypted file and decrypts it using the stored private key
Key Security Features:
- Private keys are stored securely in nilDB with proper access control
- Data is encrypted using AES-256-GCM with the user's private key
- Only authorized users can access the decryption keys
- Encrypted data is permanently stored on Arweave
Running the Complete Demo
Add a start script to your package.json:
{
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"dev": "ts-node --watch src/index.ts"
}
}Then run the complete workflow:
# Run the complete workflow
npm startThis will execute the entire process:
- Initialize the Builder and create a collection
- Generate a user and store their private key in nilDB
- Encrypt a sample file using the private key
- Upload the encrypted file to Arweave
- Download the encrypted file from Arweave
- Decrypt the file using the stored private key
nilCC
To securely compute the logic, we could take advantage of nilCC, Nillion's Confidential Computing product, that allows you to run application logic inside a TEE.
This way, all the interaction could happen inside the confidential environment with no risk of sensitive information leakage.
nilCC workloads can be easily triggered via its REST API:
curl --location '{endpoint}/api/v1/workloads/create' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--header 'x-api-key: xxx' \
--data '{
"name": "private-stamement-workload",
"artifactsVersion": "0.1.2",
"dockerCompose": "services:\n private-statements:\n image: my/workload-logic:v0.2\n environment:\n - DELEGATION_TOKENS=${DELEGATION_TOKENS}\n - COLLECTION_ID=${COLLECTION_ID}\n - DOCUMENT_ID=${DOCUMENT_ID}\n - NODE_ENV=production\n build: .\n ports:\n - \"8080:8080\"",
"envVars": {
"DOCUMENT_ID": "b05917e6-996c-4e90-a49f-b62fa891da1b",
"COLLECTION_ID": "ce9b1d1c-8006-4053-a0c8-f46ad711fc26",
"DELEGATION_TOKENS": "W3sidXJsIjoiaHR0cHM6Ly9uaWxkYi1zdGctbjEubmlsbGlvbi5uZXR3b3JrIiwidG9rZW4iOiJleUpoYkdjaU9pSkZVekkxTmtzaWZRLmV5SnBjM01pT2lKa2FXUTZibWxzT2pBeU5qTmlZMkpsTjJVeU5UZGhNamhrTmpjMk5EWTVNemc0WlRnd05EWmxNelEwTW1JeU5UVm1Zakk0TWpZME9EbGhOalE0TW1ZMk9ESmhNRFpsWWpreU1TSXNJbUYxWkNJNkltUnBaRHB1YVd3Nk1ESmxNemcwTm1NME5UVmtZbU5sWldZNVpXWm1PR0U0TkRFeU4yTXpZbVV4WWprM01UbGhZekExTkRFMVpXWmlaamN5Tnprd1pqTXhabUU1Wmpnd01qZGhJaXdpYzNWaUlqb2laR2xrT201cGJEb3dNall6WW1OaVpUZGxNalUzWVRJNFpEWTNOalEyT1RNNE9HVTRNRFEyWlRNME5ESmlNalUxWm1JeU9ESTJORGc1WVRZME9ESm1Oamd5WVRBMlpXSTVNakVpTENKbGVIQWlPakUzTlRrME56Y3hPRGtzSW1OdFpDSTZJaTl1YVd3dlpHSXZkWE5sY25NdmNtVmhaQ0lzSW1GeVozTWlPbnQ5TENKdWIyNWpaU0k2SWpRMVltUTFPVGxsTmpSbU4yVXdaVEZoWWpnNFpqVXdPVEV5TURRME5UWTFJbjAuQ05lMXlISFlUT3cyZW5lYnh4Nm56VWJaQnpJTzdKVmhYZTBMQVo4aTI3TmJzR0JHMHdBbVVGV2VKbG9oc0FxNHBXREVacGQyamtQaXNGMFZCakNHM0EiLCJwdWJsaWNLZXkiOiIwMmUzODQ2YzQ1NWRiY2VlZjllZmY4YTg0MTI3YzNiZTFiOTcxOWFjMDU0MTVlZmJmNzI3OTBmMzFmYTlmODAyN2EifSx7InVybCI6Imh0dHBzOi8vbmlsZGItc3RnLW4yLm5pbGxpb24ubmV0d29yayIsInRva2VuIjoiZXlKaGJHY2lPaUpGVXpJMU5rc2lmUS5leUpwYzNNaU9pSmthV1E2Ym1sc09qQXlOak5pWTJKbE4yVXlOVGRoTWpoa05qYzJORFk1TXpnNFpUZ3dORFpsTXpRME1tSXlOVFZtWWpJNE1qWTBPRGxoTmpRNE1tWTJPREpoTURabFlqa3lNU0lzSW1GMVpDSTZJbVJwWkRwdWFXdzZNREkxTnpreVpUazJZVFk0WXpCaU4yVm1OemM1TkRrMk1ETXlOMlJqTlRjd056QTBZelprWkRVMk5XTm1NbU5oWTJZeU1EWmlaR00zTW1RMk1USXpaamt3SWl3aWMzVmlJam9pWkdsa09tNXBiRG93TWpZelltTmlaVGRsTWpVM1lUSTRaRFkzTmpRMk9UTTRPR1U0TURRMlpUTTBOREppTWpVMVptSXlPREkyTkRnNVlUWTBPREptTmpneVlUQTJaV0k1TWpFaUxDSmxlSEFpT2pFM05UazBOemN4T0Rrc0ltTnRaQ0k2SWk5dWFXd3ZaR0l2ZFhObGNuTXZjbVZoWkNJc0ltRnlaM01pT250OUxDSnViMjVqWlNJNklqVmhNMlV4WW1aaU9ETm1OVGN5WkdVeE5tRTBOV0U1TlRsaFpETXpPREZrSW4wLjE5Nk9uVWZYT3ZnNDVnbHNUaThkZ09OWkc2R3E2NXMxVlBFa01La1huVEZCUlFmamV1R1JzTkVHUnJYci1obmp1Z1BIeWlpWmJiT0JzMU9ndkYzS053IiwicHVibGljS2V5IjoiMDI1NzkyZTk2YTY4YzBiN2VmNzc5NDk2MDMyN2RjNTcwNzA0YzZkZDU2NWNmMmNhY2YyMDZiZGM3MmQ2MTIzZjkwIn0seyJ1cmwiOiJodHRwczovL25pbGRiLXN0Zy1uMy5uaWxsaW9uLm5ldHdvcmsiLCJ0b2tlbiI6ImV5SmhiR2NpT2lKRlV6STFOa3NpZlEuZXlKcGMzTWlPaUprYVdRNmJtbHNPakF5TmpOaVkySmxOMlV5TlRkaE1qaGtOamMyTkRZNU16ZzRaVGd3TkRabE16UTBNbUl5TlRWbVlqSTRNalkwT0RsaE5qUTRNbVkyT0RKaE1EWmxZamt5TVNJc0ltRjFaQ0k2SW1ScFpEcHVhV3c2TURNd05EQXdNVFU1TW1NelpESmhOR0ZtTkdaa01EUTVaamMxWVRVMk1qTmxNVEE1TXpsaU16ZGpNemhqWXpZMFl6STJORGd3TVdFMU5UWTNZalE1TTJGaUlpd2ljM1ZpSWpvaVpHbGtPbTVwYkRvd01qWXpZbU5pWlRkbE1qVTNZVEk0WkRZM05qUTJPVE00T0dVNE1EUTJaVE0wTkRKaU1qVTFabUl5T0RJMk5EZzVZVFkwT0RKbU5qZ3lZVEEyWldJNU1qRWlMQ0psZUhBaU9qRTNOVGswTnpjeE9Ea3NJbU50WkNJNklpOXVhV3d2WkdJdmRYTmxjbk12Y21WaFpDSXNJbUZ5WjNNaU9udDlMQ0p1YjI1alpTSTZJak5rTXpKaVlXSmhaV1ZsTkRjNU5qY3dNbUUyWW1aa016RTVNV013TURaa0luMC5qYTlzQWpJRUFJb09FM2ZmcnIwV190aXlLNWZWWVhQbVdNWjIxbHlhb0d3eUZoMzQycnFhNldUSHhkT3dVSHVXdVhRR0FCOEZTQnhRWks4NXFLbGQtUSIsInB1YmxpY0tleSI6IjAzMDQwMDE1OTJjM2QyYTRhZjRmZDA0OWY3NWE1NjIzZTEwOTM5YjM3YzM4Y2M2NGMyNjQ4MDFhNTU2N2I0OTNhYiJ9XQ=="
},
"publicContainerName": "my-workload",
"publicContainerPort": 8080,
"memory": 1024,
"cpus": 1,
"disk": 10,
"gpus": 0,
"workloadId": "88384328-3038-4a8e-8d45-bbebf6d748d4",
"creditRate": 1,
"status": "scheduled",
"accountId": "some-account"
}'or the nilCC Workload Manager.
Full instructions can be found here
Additional Resources
For reference and additional examples, you can also check out the official demo repository:
This repository contains the complete working implementation that this guide is based on.
Next Steps
Try the Demo
Clone and run the complete working example from the official repository.
Advanced Uploading with Turbo
Learn advanced uploading techniques for large encrypted datasets and batch processing.
Nillion Private Storage
Deep dive into Nillion's nilDB for secure key storage and management.
SecretVaults SDK Docs
Learn more about Nillion's native access control and delegation tokens.
Need Help?
If you're interested in implementing encrypted data storage with Nillion, join our Discord community or explore the Nillion developer resources for detailed implementation guides.
How is this guide?
Storing DePIN Data on Arweave Using Turbo
Complete guide to storing and accessing DePIN network data permanently on Arweave using Turbo and AR.IO Network
Hosting Decentralised Apps on AR.IO Network
Learn how to deploy permanent, censorship-resistant websites and applications to Arweave with ArNS domain integration