@vue-solana/nuxt

Nuxt module for Solana applications.

@vue-solana/nuxt installs the Vue Solana plugin in Nuxt apps and auto-imports composables.

Install

npx nuxt module add @vue-solana/nuxt

This installs the package and adds @vue-solana/nuxt to the modules array in nuxt.config.ts.

The package is ESM only. Nuxt already bundles ESM and needs no change; a CommonJS require("@vue-solana/nuxt") fails with No "exports" main defined, so make the importing module ESM, or stay on @vue-solana/nuxt@^2, which still ships a .cjs build. See Upgrading v2 to v3.

Browser apps that create or serialize transactions can initialize the Buffer polyfill from @vue-solana/nuxt/buffer-polyfill. Use @vue-solana/nuxt/kit for the Kit API — it re-exports all of @solana/kit — or the auto-imported useSolanaClient().

Module Setup

export default defineNuxtConfig({
  modules: ["@vue-solana/nuxt"],
  solana: {
    cluster: "devnet",
  },
});

You can also configure a custom RPC endpoint:

export default defineNuxtConfig({
  modules: ["@vue-solana/nuxt"],
  solana: {
    cluster: "mainnet",
    endpoint: "https://your-rpc.example.com",
    commitment: "confirmed",
  },
});

Supported clusters are mainnet (legacy alias mainnet-beta), devnet, testnet, and localnet. Use mainnet for Solana mainnet; this is Solana's official mainnet cluster name.

Nuxt module options are stored in public runtime config, so they must be JSON-serializable. Custom wallet adapter objects are intentionally excluded from Nuxt config; use the Vue plugin directly in client-only Vue code if you need to inject a custom wallet object.

ModuleOptions intentionally omits payer and payerSecretKey as well. They remain supported by direct @vue-solana/core and @vue-solana/vue clients, but the Nuxt module does not forward either field and strips it from public runtime config. Never put a raw secret, seed phrase, or payerSecretKey in nuxt.config.ts or runtimeConfig.public: those values are visible to the browser. For a client-owned demo or relayer signer, create it in a client-only plugin with generateKeyPairSigner() and install the Vue plugin there, setting clientPlugin: false so the module skips its own plugin. Installing both would create two Solana contexts, two wallet subscriptions, and two connection checks, and only the last provide would win.

The module's client runtime plugin also installs the app-wide selected wallet account context automatically, so useSolanaSelectedWalletAccount() works in every component without mounting a provider. To customize persistence (stateSync) or filtering (filterWallet), mount SelectedWalletAccountProvider from @vue-solana/vue/useSelectedWalletAccount deeper in the tree to shadow the default context.

Mobile wallet options are safe to configure in nuxt.config.ts when they contain only JSON-serializable app identity and redirect settings:

export default defineNuxtConfig({
  modules: ["@vue-solana/nuxt"],
  solana: {
    cluster: "devnet",
    mobileWallet: {
      appIdentity: {
        name: "My Nuxt Solana App",
        uri: "https://example.com",
        icon: "favicon.ico",
      },
    },
    iosWallet: {
      appIdentity: {
        name: "My Nuxt Solana App",
      },
      redirectUrl: "https://example.com",
    },
  },
});

Pass mobileWallet: false or iosWallet: false to disable either mobile wallet source. The module also pre-optimizes common Solana, Wallet Adapter, and mobile wallet dependencies so Vite can bundle browser transaction and wallet code correctly.

Auto-Imported Composables

The module auto-imports these composables from direct @vue-solana/vue/* subpaths rather than the root Vue package barrel. This keeps Nuxt SSR bundles from pulling in unrelated Solana runtime code just because a page uses one composable.

  • useSolana(): returns the full injected Solana context.
  • useSolanaClient(): returns the Kit { client, rpc } from the context. Recommended for new code.
  • useSolanaRpc(): returns cluster, endpoint, RPC status, latest blockhash, the injected Kit client, and checkConnection().
  • useSolanaConnection(): returns the injected Kit client (deprecated in favor of useSolanaClient()).
  • useSolanaAccountInfo(address, options?): reads account info and can subscribe to account changes.
  • useSolanaAirdrop(): airdrops SOL into an account on test networks and local validators.
  • useSolanaWallet(): returns selected wallet state, connection state, capabilities, and wallet actions.
  • useSolanaWallets(): returns discovered wallets and wallet selection/refresh actions.
  • useSolanaBalance(address, commitment?): reads lamport balance for a public key or address.
  • useSolanaTokenAccounts(owner, options?): reads all SPL token accounts for an owner, querying both Token and Token-2022 programs by default.
  • useSolanaTokenBalance(mint, owner): reads the SPL token balance and decimals for a mint/owner pair via the associated token account.
  • useSolanaProgramAccounts(programId, options?): reads program-owned accounts with filters and data slicing.
  • useSolanaTransactionConfirmation(options?): confirms an existing transaction signature.
  • useSolanaSignatureStatus(signature, options?): reads, polls, or subscribes to signature status.
  • useSolanaSignMessage(): signs off-chain authentication or ownership challenge messages.
  • useSolanaSignAndSendTransaction(): signs, sends, and optionally confirms transactions.
  • useSolanaAction(handler): generic async action state machine with abort-on-redispatch.
  • useSolanaRequest(source, options?): one-shot request that re-fires when its source changes, with stale-while-revalidate.
  • useSolanaSubscription(source, options?): live data from RPC subscriptions and other reactive stream sources.
  • useSolanaTrackedData(source, options?): RPC subscription seeded by a one-shot fetch, slot-deduplicated.
  • useSolanaSignIn(): triggers a wallet's Sign In With Solana (SIWS) feature.

For cache keying across mounts, the SWR adapters are not auto-imported. Import them explicitly from the Vue package subpath:

import { useRequestSwr } from "@vue-solana/vue/swr";

const balance = useRequestSwr(`balance:${someAddress}`, rpc.getBalance(someAddress));

See Data Fetching Composables for the full semantics of the request, subscription, tracked-data, and SWR-cache composables.

  • useSolanaSelectedWalletAccount(): reads the app-wide selected wallet account context installed by the runtime plugin.
  • useSolanaSignTransactions(): signs multiple transactions in one wallet request.
  • useSolanaSignAndSendTransactions(): signs and sends multiple transactions in one wallet request.
  • useSolanaPayer() / useSolanaIdentity(): reactive Kit client signers (requires a signer plugin).
  • useSolanaPlanTransaction() / useSolanaPlanTransactions(): plan transaction messages from instruction inputs.
  • useSolanaSendTransaction() / useSolanaSendTransactions(): use the official Kit planner and RPC plan-sending executor installed by the default client; they plan, sign, submit, and wait for confirmed commitment with no wallet popup. Configure a payer in a client-only Vue plugin; use useSolanaSignAndSendTransaction(s) when a connected wallet must approve each transaction.

These are Nuxt aliases for the Vue composables.

The default client created by the Nuxt module composes the official solanaRpc() and rpcAirdrop() plugins; solanaRpc() installs the transaction planner and the plan-signing and plan-sending executors itself. The old custom fallback sender is not used. useSolanaSendTransaction() and useSolanaSendTransactions() therefore expose the official client-sending capability, but the module does not configure a payer because payer and payerSecretKey are intentionally omitted from ModuleOptions, so a client-sent transaction needs a payer installed in a client-only plugin with clientPlugin: false, as described in Module Setup. Their sent status is reached after the official executor completes its send-and-confirm operation at confirmed commitment.

The Vue package uses short names such as useRpc() because callers import them explicitly from @vue-solana/vue/useRpc.

The Nuxt module exposes prefixed names such as useSolanaRpc() because auto-imported composables share the app-wide Nuxt namespace and should avoid collisions with app code or other modules.

useSolana() is the exception because it is already namespaced and acts as the canonical context accessor in both Vue and Nuxt.

Use the useSolana* names inside Nuxt apps so auto-imports work without explicit imports.

Raw transaction bytes and the browser Buffer helper are explicit imports, not auto-imports:

import { installSolanaBufferPolyfill } from "@vue-solana/nuxt/buffer-polyfill";

The Kit API is available both as an auto-import and as an explicit import:

import { address, lamports } from "@vue-solana/nuxt/kit";

const { client, rpc } = useSolanaClient();
const slot = await rpc.getSlot().send(); // bigint
const owner = address("PASTE_A_SOLANA_ADDRESS");

Use direct @vue-solana/core/* imports only for lower-level core usage.

Direct package subpaths:

  • @vue-solana/nuxt/buffer-polyfill
  • @vue-solana/nuxt/kit

The runtime plugin is client-only. Auto-imported composables can be called during SSR and return inert state until hydration provides the real client context. Trigger RPC and wallet work from client lifecycle hooks or user actions.

Android Mobile Wallet Adapter registration also runs only on the client. On Android Chrome and Chrome PWAs, Mobile Wallet Adapter can appear in the same useSolanaWallets() list as browser extension wallets. On iOS browsers, Phantom, Solflare, and Backpack can appear in the same list through wallet-specific universal links. Desktop native app wallet adapters are planned but not implemented yet.

  • RPC and Clusters: configure the Nuxt module and read RPC state.
  • Wallets: use useSolanaWallets() and useSolanaWallet() safely in client flows.
  • Account Reads: read balances, account data, program accounts, and signature status.
  • Transactions: sign, send, confirm, and handle transaction status from Nuxt.
  • Message Signing: request wallet signatures for off-chain messages.
  • E2E Testing: mock RPC, RPC subscriptions, and wallets in Playwright tests.
  • Errors: map auto-imported composable errors to safe UI messages.

Read RPC State

<script setup lang="ts">
const { cluster, endpoint, status, error, latestBlockhash, checkConnection } = useSolanaRpc();

const rpcErrorMessage = computed(() => {
  if (!error.value) return null;
  return error.value.code === "RPC_FAILURE"
    ? "Unable to reach the configured Solana RPC endpoint."
    : "Unable to check the Solana connection.";
});
</script>

<template>
  <section>
    <p>Cluster: {{ cluster }}</p>
    <p>Endpoint: {{ endpoint }}</p>
    <p>Status: {{ status }}</p>
    <p>Latest blockhash: {{ latestBlockhash }}</p>
    <p v-if="rpcErrorMessage">{{ rpcErrorMessage }}</p>
    <button type="button" @click="checkConnection">Check RPC</button>
  </section>
</template>

Read Balance

<script setup lang="ts">
const address = ref("PASTE_A_SOLANA_ADDRESS");
const { balance, loading, error, refresh } = useSolanaBalance(address);

const balanceErrorMessage = computed(() => {
  switch (error.value?.code) {
    case "INVALID_ADDRESS":
      return "Enter a valid Solana address.";
    case "RPC_FAILURE":
      return "Unable to load the balance from RPC.";
    default:
      return null;
  }
});
</script>

<template>
  <section>
    <p>Lamports: {{ balance }}</p>
    <p v-if="loading">Loading...</p>
    <p v-if="balanceErrorMessage">{{ balanceErrorMessage }}</p>
    <button type="button" @click="refresh">Refresh</button>
  </section>
</template>

Read Token Accounts

<script setup lang="ts">
const owner = ref("PASTE_A_SOLANA_ADDRESS");
const { tokenAccounts, loading, error, refresh } = useSolanaTokenAccounts(owner);

const tokenErrorMessage = computed(() => {
  switch (error.value?.code) {
    case "INVALID_ADDRESS":
      return "Enter a valid Solana address.";
    case "RPC_FAILURE":
      return "Unable to load token accounts from RPC.";
    default:
      return null;
  }
});
</script>

<template>
  <section>
    <p>Token accounts: {{ tokenAccounts.length }}</p>
    <ul>
      <li v-for="(account, i) in tokenAccounts" :key="i">
        {{ account.mint }} — {{ account.amount }}
      </li>
    </ul>
    <p v-if="loading">Loading...</p>
    <p v-if="tokenErrorMessage">{{ tokenErrorMessage }}</p>
    <button type="button" @click="refresh">Refresh</button>
  </section>
</template>

useSolanaTokenAccounts() clears state without calling RPC when the owner is null. Pass programId in the options to limit results to a single token program.

Read Token Balance

<script setup lang="ts">
const mint = ref("PASTE_A_MINT_ADDRESS");
const owner = ref("PASTE_A_SOLANA_ADDRESS");
const { balance, decimals, loading, error, refresh } = useSolanaTokenBalance(mint, owner);

const tokenBalanceErrorMessage = computed(() => {
  switch (error.value?.code) {
    case "INVALID_ADDRESS":
      return "Enter valid mint and owner addresses.";
    case "RPC_FAILURE":
      return "Unable to load token balance from RPC.";
    default:
      return null;
  }
});
</script>

<template>
  <section>
    <p v-if="balance !== null">Balance: {{ balance }} ({{ decimals }} decimals)</p>
    <p v-else>No token account found.</p>
    <p v-if="loading">Loading...</p>
    <p v-if="tokenBalanceErrorMessage">{{ tokenBalanceErrorMessage }}</p>
    <button type="button" @click="refresh">Refresh</button>
  </section>
</template>

useSolanaTokenBalance() returns null balance and decimals when the associated token account does not exist, without treating it as an error. Its balance and decimals are read-only computed refs derived from one read — read them, do not assign to them.

Read Composable Semantics

useSolanaBalance(), useSolanaAccountInfo(), useSolanaProgramAccounts(), useSolanaTokenAccounts(), and useSolanaTokenBalance() share one state machine, identical to the Vue composables:

  • refresh() rejects on failure instead of resolving, and resolves with null when an input is empty. await refresh() without a try/catch throws. Wiring it straight to @click is fine — Vue swallows the rejection — but hand-written callers must handle it. useSolanaRequest() is the exception and still resolves with the attempt result.
  • Data drops back to its empty value when a read fails. The last successful balance does not linger beside a fresh error. Branch on error to choose what to render.

An unparseable address reports INVALID_ADDRESS in error and never reaches the RPC. See Account Reads.

Error Handling

Nuxt auto-imported composables expose the same normalized SolanaError | null refs as @vue-solana/vue. Use stable error.value.code values for UI branches and keep error.value.cause for logging original wallet, RPC, parsing, timeout, or storage failures.

<script setup lang="ts">
const { error, execute } = useSolanaSignAndSendTransaction();

const message = computed(() => {
  switch (error.value?.code) {
    case "NO_WALLET_SELECTED":
      return "Choose a wallet first.";
    case "USER_REJECTED":
      return "The wallet request was rejected.";
    case "TRANSACTION_TIMEOUT":
      return "The transaction is taking longer than expected.";
    case "RPC_FAILURE":
      return "The Solana RPC request failed.";
    default:
      return null;
  }
});
</script>

Read Account Data

<script setup lang="ts">
const address = ref("PASTE_A_SOLANA_ADDRESS");
const programId = ref("PASTE_A_SOLANA_PROGRAM_ID");
const signature = ref("PASTE_A_TRANSACTION_SIGNATURE");

const account = useSolanaAccountInfo(address, { watch: true });
const programAccounts = useSolanaProgramAccounts(programId, {
  dataSlice: { offset: 0, length: 32 },
  filters: [{ dataSize: 165 }],
});
const signatureStatus = useSolanaSignatureStatus(signature, { pollIntervalMs: 2_000 });
</script>

Use useSolanaProgramAccounts() carefully on public RPC nodes. Prefer narrow filters, use dataSlice for partial reads, and avoid polling broad scans.

Wallet State

<script setup lang="ts">
const { wallets, selectedWallet, refreshWallets, selectWallet } = useSolanaWallets();
const { publicKey, connected, connect, disconnect } = useSolanaWallet();
</script>

<template>
  <section>
    <button type="button" @click="refreshWallets">Refresh Wallets</button>

    <button
      v-for="wallet in wallets"
      :key="wallet.name"
      type="button"
      @click="selectWallet(wallet)"
    >
      {{ wallet.name }}
    </button>

    <p>Selected: {{ selectedWallet?.name ?? "None" }}</p>
    <p>Connected: {{ connected }}</p>
    <p>Public key: {{ publicKey }}</p>
    <button type="button" :disabled="!selectedWallet || connected" @click="connect">Connect</button>
    <button type="button" :disabled="!connected" @click="disconnect">Disconnect</button>
  </section>
</template>

Browser extension wallets are discovered through the Solana Wallet Standard. Android Mobile Wallet Adapter wallets are registered through @solana-mobile/wallet-standard-mobile on supported Android Chrome clients and exposed through the same wallet list. iOS Phantom, Solflare, and Backpack entries are exposed through wallet-specific universal links on iOS browsers. refreshWallets() only updates the discovered wallet list, and selectWallet() only configures the active wallet. connected remains false until connect() succeeds, even if the extension exposes previously authorized accounts after a page refresh.

Message Signing

<script setup lang="ts">
const { connected, canSignMessage } = useSolanaWallet();
const { signature, status, error, execute } = useSolanaSignMessage();

if (connected.value && canSignMessage.value) {
  await execute(new TextEncoder().encode("Sign in to example.com"));
}
</script>

Message signing is for wallet ownership or authentication challenges. It is not transaction signing and does not authorize on-chain state changes. Wallets that do not expose message signing report canSignMessage as false and execute() rejects with an unsupported-wallet error.

Sign, Send, And Confirm A Transaction

Use useSolanaSignAndSendTransaction() from a client-side user action when the connected wallet should sign and submit a transaction. Pass confirm: true when the UI should wait for confirmation instead of stopping after signature submission.

<script setup lang="ts">
import type { SolanaTransaction } from "@vue-solana/nuxt/kit";

const { connected, canSignTransaction } = useSolanaWallet();
const { signature, confirmation, status, loading, error, execute } =
  useSolanaSignAndSendTransaction();

const canSubmit = computed(() => connected.value && canSignTransaction.value && !loading.value);

async function submitTransaction(transaction: SolanaTransaction) {
  // Build the transaction message with @vue-solana/nuxt/kit and serialize it to wire bytes first.
  await execute(transaction, {
    confirm: true,
    confirmation: { commitment: "confirmed", timeoutMs: 120_000 },
  });
}
</script>

<template>
  <section>
    <button type="button" :disabled="!canSubmit" @click="submitTransaction">
      Send transaction
    </button>
    <p>Status: {{ status }}</p>
    <p v-if="signature">Submitted: {{ signature }}</p>
    <p v-if="confirmation">Confirmed at {{ confirmation.commitment }}</p>
    <p v-if="error">Unable to complete the transaction.</p>
  </section>
</template>

Status moves from sending to sent after RPC submission. When confirmation is enabled it then moves through confirming and ends at the reached commitment, such as confirmed or finalized. If confirmation times out after submission, signature remains available so the app can show an explorer link or poll signature status before retrying.

Client-sent transactions are different: useSolanaSendTransaction() and useSolanaSendTransactions() use the official RPC plan-sending executor. The executor waits for confirmed commitment before execute() resolves, so their sent status means the client-side send-and-confirm operation completed. They show no wallet popup; install a payer in a client-only Vue plugin. Do not try to configure a raw payerSecretKey in Nuxt public runtime config.

Wallet prompts must be triggered by user interaction after hydration. Do not call execute() during SSR, in server routes, or automatically on page load.

Confirm An Existing Signature

Use useSolanaTransactionConfirmation() when you already have a signature and want reactive confirmation state.

<script setup lang="ts">
const signature = ref("PASTE_TRANSACTION_SIGNATURE");
const { confirmation, status, error, confirm } = useSolanaTransactionConfirmation({
  commitment: "confirmed",
  timeoutMs: 60_000,
});

async function confirmCurrentSignature() {
  await confirm(signature.value);
}
</script>

<template>
  <section>
    <button type="button" @click="confirmCurrentSignature">Confirm signature</button>
    <p>Status: {{ status }}</p>
    <p v-if="confirmation">Reached {{ confirmation.commitment }}</p>
    <p v-if="error">Unable to confirm the signature.</p>
  </section>
</template>

Track Signature Status

Use useSolanaSignatureStatus() when you need ongoing status checks for a submitted signature. This is useful after a timeout because a transaction might still land after the UI stopped waiting.

<script setup lang="ts">
const signature = ref("PASTE_TRANSACTION_SIGNATURE");
const { status, loading, error, refresh, stopPolling, stopSubscription } = useSolanaSignatureStatus(
  signature,
  {
    pollIntervalMs: 2_000,
  },
);

onBeforeUnmount(() => {
  stopPolling();
  void stopSubscription();
});
</script>

For explorer links, use the configured cluster. Devnet links should include ?cluster=devnet; both mainnet and the legacy mainnet-beta alias should omit the cluster query.

function explorerUrl(signature: string, cluster: string) {
  const suffix = cluster === "mainnet" || cluster === "mainnet-beta" ? "" : `?cluster=${cluster}`;
  return `https://explorer.solana.com/tx/${signature}${suffix}`;
}

Example App

For a complete runnable Nuxt flow, see the Nuxt example.