ar.io Logoar.io Documentation

From Build to Permanent App

This guide connects the last mile of shipping a web app: turn a static build into a permanent Turbo upload, then publish it at a human-readable ArNS name. You only need a Solana wallet, and the same URL can follow every release even though each upload remains immutable.

How Turbo and ArNS Fit Together

Static build folder


Turbo uploads files + creates a manifest


Permanent manifest transaction ID


ArNS record points your name to that ID


https://yourname.ar.io
  • Turbo stores the build files and creates an Arweave manifest that routes paths such as /, /assets/app.js, and /about/.
  • ArNS gives that immutable manifest a stable name. On the next release, update the name to the new manifest ID instead of changing the URL you share.
  • ar.io gateways resolve the name and serve the manifest's files. The app is also available directly by its transaction ID, independently of ArNS.

Prefer one command?

ARIO Deploy automates the same upload, manifest, and ArNS update workflow. Continue here if you want to understand and control each SDK operation yourself.

What You'll Build

By the end of this guide, your app will be:

  • Permanently stored on Arweave (can never be deleted or modified)
  • Addressable by transaction ID so every deployed version can still be retrieved
  • Accessible at https://yourname.ar.io (and through other ar.io gateways)
  • Owned by you as a Metaplex Core NFT (the ArNS name token)

Prerequisites

  • Node.js 18+
  • A Solana wallet with SOL and ARIO tokens
  • A built web app (any framework that outputs a static folder — React, Next.js, Vue, Svelte, etc.)
npm install @ar.io/sdk @ardrive/turbo-sdk @solana/kit bs58

Requires @ar.io/sdk version 3.23+ for Solana support.

Step-by-Step

Build Your App

Generate a static build of your web application:

# next.config.js must have: output: 'export'
npm run build
# Output: ./out/
npm run build
# Output: ./dist/
# Build your app to a static folder
npm run build
# Use whatever output directory your framework creates

Set Up Your Signer

Create a shared setup file that both Turbo (for uploads) and the ar.io SDK (for naming) can use:

// setup.ts
import { ARIO, ANT } from '@ar.io/sdk';
import { TurboFactory } from '@ardrive/turbo-sdk';
import { createKeyPairSignerFromBytes } from '@solana/kit';
import bs58 from 'bs58';
import fs from 'fs';

// Load your Solana keypair
const keypairBytes = new Uint8Array(
  JSON.parse(fs.readFileSync('./solana-keypair.json', 'utf-8')),
);

// For ar.io SDK (ArNS names, records)
export const signer = await createKeyPairSignerFromBytes(keypairBytes);
export const ario = ARIO.mainnet({ signer });

// For Turbo (file uploads to Arweave)
// Turbo uses its own signer format — pass the secret key as base58
export const turbo = TurboFactory.authenticated({
  privateKey: bs58.encode(keypairBytes.slice(0, 32)),
  token: 'solana',
});

Security: Never commit your keypair file to version control. Use a dedicated deployment wallet with only the SOL and ARIO needed for the operation.

Upload to Arweave via Turbo

Upload your build folder. Turbo bundles all files into an Arweave manifest — a single transaction ID that maps to all your app's files:

// deploy.ts
import { turbo, ario, signer } from './setup';
import { ANT } from '@ar.io/sdk';

const ARNS_NAME = 'my-cool-app'; // the ArNS name you want
const BUILD_DIR = './dist';       // your build output folder

// Step 1: Upload the build folder
console.log('Uploading to Arweave...');
const uploadResult = await turbo.uploadFolder({
  folderPath: BUILD_DIR,
  dataItemOpts: {
    tags: [
      { name: 'App-Name', value: ARNS_NAME },
      { name: 'App-Version', value: '1.0.0' },
    ],
  },
});

const manifestTxId = uploadResult.manifestResponse.id;
console.log(`Uploaded! Manifest TX: ${manifestTxId}`);
console.log(`Direct access: https://turbo-gateway.com/${manifestTxId}`);

Files under 100KB are free to upload via Turbo. Larger uploads are paid with SOL from your wallet — no pre-funding needed when using just-in-time payments.

Register an ArNS Name (If You Don't Have One)

Skip this step if you already own the ArNS name.

// Check if the name is available
try {
  const existing = await ario.getArNSRecord({ name: ARNS_NAME });
  console.log(`Name "${ARNS_NAME}" is already registered`);
} catch {
  // Name is available — register it
  console.log(`Registering "${ARNS_NAME}"...`);

  // Check the cost first
  const cost = await ario.getTokenCost({
    intent: 'Buy-Name',
    name: ARNS_NAME,
    type: 'lease',
    years: 1,
  });
  console.log(`Cost: ${cost / 1_000_000} ARIO`);

  // Buy it (this mints an ANT as a Metaplex Core NFT)
  await ario.buyRecord({
    name: ARNS_NAME,
    type: 'lease',
    years: 1,
  });

  console.log(`Registered "${ARNS_NAME}"!`);
}

Point Your Name to Your App

Set the ANT's root (@) record to your uploaded manifest:

// Get the ANT mint address from the ArNS record
const record = await ario.getArNSRecord({ name: ARNS_NAME });

// Initialize the ANT
const ant = ANT.init({ signer, processId: record.processId });

// Set the root record to your manifest
console.log('Setting ArNS record...');
await ant.setRecord({
  undername: '@',
  transactionId: manifestTxId,
  ttlSeconds: 3600,
});

console.log('Done! Your app is live at:');
console.log(`  https://${ARNS_NAME}.ar.io`);
console.log(`  https://${ARNS_NAME}.turbo-gateway.com`);

Verify

Wait a minute for gateways to pick up the new record, then verify:

curl -I https://my-cool-app.ar.io
# Should return 200 OK with your app's index.html

Verify both layers so it is easier to diagnose a problem:

# Storage and manifest routing
curl -I https://turbo-gateway.com/YOUR_MANIFEST_TX_ID

# Name resolution
curl -I https://my-cool-app.ar.io

If the transaction URL works but the ArNS URL does not, confirm that the root (@) record contains the manifest transaction ID and allow for the record's TTL. If neither URL works, check the upload result and make sure the build folder contained an index.html file.

Your app is now permanently hosted and accessible through the ar.io gateway network.

Updating Your App

Since Arweave data is immutable, "updating" means uploading a new version and updating your ArNS record to point to it:

// Upload new version
const newUpload = await turbo.uploadFolder({
  folderPath: './dist',
  dataItemOpts: {
    tags: [
      { name: 'App-Name', value: 'my-cool-app' },
      { name: 'App-Version', value: '2.0.0' },
    ],
  },
});

// Update the record
const ant = ANT.init({ signer, processId: record.processId });
await ant.setRecord({
  undername: '@',
  transactionId: newUpload.manifestResponse.id,
  ttlSeconds: 3600,
});

// Old version is still on Arweave forever — instant rollback if needed

Using Undernames for Staging

You can use undernames to deploy staging environments alongside production:

// Deploy staging version
await ant.setRecord({
  undername: 'staging',
  transactionId: stagingManifestTxId,
  ttlSeconds: 300, // short TTL for faster updates
});
// Access at: https://staging_my-cool-app.ar.io

// When ready, promote to production
await ant.setRecord({
  undername: '@',
  transactionId: stagingManifestTxId,
  ttlSeconds: 3600,
});

Full Script

Here's the complete deployment script you can adapt:

import { ARIO, ANT } from '@ar.io/sdk';
import { TurboFactory } from '@ardrive/turbo-sdk';
import { createKeyPairSignerFromBytes } from '@solana/kit';
import bs58 from 'bs58';
import fs from 'fs';

const ARNS_NAME = process.argv[2] || 'my-app';
const BUILD_DIR = process.argv[3] || './dist';

async function deploy() {
  // Load Solana keypair
  const keypairBytes = new Uint8Array(
    JSON.parse(fs.readFileSync('./solana-keypair.json', 'utf-8')),
  );

  // Init SDKs
  const signer = await createKeyPairSignerFromBytes(keypairBytes);
  const ario = ARIO.mainnet({ signer });
  const turbo = TurboFactory.authenticated({
    privateKey: bs58.encode(keypairBytes.slice(0, 32)),
    token: 'solana',
  });

  // Upload
  console.log(`Uploading ${BUILD_DIR}...`);
  const upload = await turbo.uploadFolder({
    folderPath: BUILD_DIR,
    dataItemOpts: {
      tags: [{ name: 'App-Name', value: ARNS_NAME }],
    },
  });
  const txId = upload.manifestResponse.id;
  console.log(`Uploaded: https://turbo-gateway.com/${txId}`);

  // Get ANT and update record
  const arnsRecord = await ario.getArNSRecord({ name: ARNS_NAME });
  const ant = ANT.init({ signer, processId: arnsRecord.processId });

  console.log('Updating ArNS record...');
  await ant.setRecord({
    undername: '@',
    transactionId: txId,
    ttlSeconds: 3600,
  });

  console.log(`\nLive at: https://${ARNS_NAME}.ar.io`);
}

deploy().catch(console.error);
# Usage
npx tsx deploy.ts my-cool-app ./dist

Next Steps

How is this guide?