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 bs58Requires @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 createsSet 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.htmlVerify 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.ioIf 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 neededUsing 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 ./distNext Steps
ArNS Name Management
Manage records, undernames, and transfer ownership
Version Control with Undernames
Use undernames for staging, rollbacks, and multi-version deployments
Turbo Upload Options
Advanced upload features — encryption, tagging, folder uploads
How is this guide?