Next.js dApp Integration
A Solana dApp on Next.js is a split-runtime product: Server Components and Route Handlers on the Node side, wallet-bound Client Components in the browser, and @solana/kit 7.0.0 talking to RPC (and often WebSocket) on a chosen cluster.
This page is the section umbrella. It maps App Router + Solana providers, client-side signing, API routes for transactions, displaying on-chain data, and caching / realtime so sibling how-tos read as zoom levels of one stack.
Summary
- Integrate Solana into Next.js App Router by isolating wallet and browser APIs behind one client provider tree, building and signing user-authority transactions only in the browser, using Route Handlers for secrets and sponsorship, and hydrating UI from RPC (with cache + subscriptions) under an explicit commitment policy.
- Insight: Mixing SSR with
window, putting keypairs inNEXT_PUBLIC_env, or treatingsendTransactionsuccess as settlement produces broken builds, drained sponsors, and false UX. A shared map of server vs client responsibilities prevents those failure modes. - Key Concepts: App Router, Server Component, Client Component, Solana provider tree, wallet adapter / wallet-standard, @solana/kit RPC, transaction message, blockhash lifetime, client signing, Route Handler, fee payer / sponsorship, commitment, SWR / React Query, accountSubscribe.
- When to Use: Scaffolding a greenfield Solana dApp on Next.js 13+ App Router; onboarding a React team to wallet write paths; deciding what runs on the server versus in the wallet; planning portfolio UIs and live balances.
- Limitations/Trade-offs: Client providers and wallet modals cannot render purely on the server; sponsored routes need auth, rate limits, and spend caps; aggressive polling burns RPC; WebSockets are node-local views and still require commitment discipline.
- Related Topics: dApp layout basics, providers and App Router, client-side signing, API routes for transactions, displaying on-chain data, caching and realtime.
Foundations
Next.js App Router is Server Components-first: files under app/ are server by default, and "use client" marks islands that hydrate in the browser.
Wallet SDKs need window, storage, and user gestures that do not exist during SSR. The rule is structural: wallet context, connect UI, and signing live in client components; marketing shells and secret-holding handlers stay on the server.
Solana providers sit in one client wrapper (for example app/solana-provider.tsx) composed into the root layout. That tree typically supplies:
- RPC / kit config - cluster HTTP URL (often
NEXT_PUBLIC_RPC_URL). - Wallet registry - adapters or wallet-standard discovery (Phantom, Solflare, and others).
- Modal / connect UI - one modal provider so nested routes do not double-mount dialogs.
Root layout.tsx can stay a Server Component that only wraps children with that provider. Nested layouts should not re-declare the same wallet stack.
@solana/kit 7.0.0 is the recommended client for createSolanaRpc, createSolanaRpcSubscriptions, transaction messages, and confirmation helpers. Legacy web3.js v1 appears only in migrations; new work targets kit.
Cluster and env matter from day one. NEXT_PUBLIC_* ships to the browser (RPC URL, cluster label, public program ids). Sponsor keypairs and privileged API secrets stay server-only (no NEXT_PUBLIC_ prefix), ideally in a secrets manager.
Commitment (processed, confirmed, finalized) matches the rest of this site: optimistic UI may use shallower levels; irreversible off-chain effects wait deeper.
A minimal shape of the stack:
app/layout.tsx (Server Component)
|
+-- SolanaProvider ("use client")
| Connection / kit config
| WalletProvider + modal
|
+-- page.tsx (Server OK for public reads / SEO)
|
+-- client islands (balance chips, send buttons)
|
+-- app/api/**/route.ts (Route Handlers)
server builds / sponsors / Actions JSON
Sibling pages deepen each box; this page keeps the whole diagram in view.
Mechanics & Interactions
Providers and the App Router boundary
On first load, Server Components render HTML without wallet state. After hydration, providers attach connection and wallet context. Hooks such as useWallet or wallet-standard helpers run only under that client boundary.
Practical rules: import wallet CSS once; stabilize adapters with useMemo; keep one root provider (duplicates double modals); split or dynamically import with ssr: false if a widget still pulls browser-only code into a server graph.
Client-side signing
User-authority work (user-paid transfers, mints, DeFi approvals) follows a fixed Client Component loop:
- Ensure wallet is connected and the fee payer pubkey is known.
- Fetch a fresh blockhash (
getLatestBlockhash). - Build a versioned transaction message with kit (fee payer, lifetime, instructions).
- Obtain Ed25519 signatures from the wallet (kit signer bridge or adapter paths).
- Send and wait for commitment (
sendAndConfirmTransactionFactorywith HTTP + WSS, or equivalent).
Secrets never leave the wallet. Simulation before the modal reduces avoidable failures; blockhash expiry during slow approval requires rebuild and re-prompt.
See Client-Side Signing.
API routes for transactions
Route Handlers (app/api/.../route.ts) run on the server when you must:
- Hold a sponsor / paymaster keypair as fee payer for gasless onboarding.
- Enforce business rules, allowlists, or inventory before offering a message to the wallet.
- Implement Solana Actions (HTTPS Actions JSON for wallets and Blinks).
- Hide third-party API keys used while assembling instructions.
A common pattern is partial construction: server builds (and may partial-sign as fee payer), returns serialized bytes, and the user wallet co-signs. Pure server signing is only for accounts the server fully controls.
Protect endpoints: schema-validate JSON, authenticate, rate-limit, cap sponsorship, never return key material.
See API Routes for Transactions.
Displaying on-chain data
Rendering chain state is read-path RPC plus careful formatting:
| UI need | Typical source |
|---|---|
| SOL balance | getBalance |
| SPL token balances | ATAs + token decode / mint decimals |
| NFTs / cNFTs | DAS provider (getAsset, getAssetsByOwner) when available |
| Custom program accounts | kit / Codama fetch helpers, PDA derivation |
Keep lamports and token base units as integer / bigint until the display string. Distinguish loading, empty, and error. Server Components can seed public SEO snapshots; wallet-private portfolios stay client-side.
Caching and realtime
Two layers often coexist:
- Client HTTP cache - SWR or React Query with keys that include cluster + pubkey. Optional
refreshIntervalas a coarse fallback. - WebSocket subscriptions -
accountSubscribe(and signature subscriptions) via kit; on notify,mutatethe cache.
RSC / Next data cache is not the browser wallet cache. After your own confirmed write, invalidate related keys. Abort subscriptions on unmount. Back off on 429.
See Caching & Realtime.
End-to-end write path (self-custody)
User clicks Send (Client Component)
|
v
kit: build message + recent blockhash
|
v
Wallet modal signs (Ed25519, secrets stay in wallet)
|
v
sendTransaction via RPC --> signature S
|
+-- WSS signatureSubscribe / confirm factory
|
v
commitment: processed -> confirmed -> finalized
|
v
invalidate SWR keys; show explorer link
Sponsored flows insert a Route Handler between click and wallet: server returns a partial message; the client still collects the user signature when user authority is required.
Advanced Considerations & Applications
Section concept map
| Concern | Primary page | Builder takeaway |
|---|---|---|
| Folder layout, env, read/write split | dApp Integration Basics | Structure before features |
| Client provider tree, SSR safety | Providers & App Router | One root provider; server layout shell |
| Build, sign, confirm in browser | Client-Side Signing | Fresh blockhash; no server wallet |
| Balances, tokens, program state UI | Displaying On-Chain Data | bigint until format; explicit states |
| Server-built / sponsored txs | API Routes for Transactions | Secrets server-only; caps and auth |
| SWR + subscriptions | Caching & Realtime | Mutate after confirm; abort on unmount |
Server-side reads, transaction UX, and best practices refine polish; the rows above are the integration core.
Designing the split: server vs client
| Work | Prefer client | Prefer server (Route Handler / RSC) |
|---|---|---|
| Connect wallet, sign as user | Yes | No |
| Public program config for SEO | Optional seed | Yes |
| Sponsor fee payer | Co-sign only | Yes for key material |
| Private RPC API key | No | Yes (proxy) |
| Live wallet portfolio | Yes + WS | Initial public snapshot only |
Do not "move signing to the server" to simplify React; that redefines custody.
Production posture
- Pin cluster, program ids, and RPC/WSS URLs per environment.
- Log correlation ids on API routes; show decoded simulation errors, not stack traces.
- Prefer paid RPC on mainnet; align wallet network with RPC cluster.
- Treat Actions / Blinks as first-class HTTPS products when you expose unattended construction.
Tooling alignment
Target @solana/kit 7.0.0 for RPC, messages, and confirmation. Bridge wallet-adapter or wallet-ui signers into kit rather than parallel web3.js v1 stacks. Integration tests can use Surfpool or a local validator; unit tests mock RPC. Co-developed programs on this site assume Anchor 0.32.1, Rust 1.91.1, Agave 4.1.1, and Solana CLI 3.0.10.
Common Misconceptions
- "I can call useWallet from any Server Component if I am careful." Wallet hooks require a client boundary and browser APIs; importing them into a server graph fails the build or crashes SSR.
- "Putting the sponsor keypair in NEXT_PUBLIC_ env is fine if the repo is private." Public env is shipped to every browser; treat sponsor keys as server-only secrets with rotation and spend limits.
- "sendTransaction returning a signature means the user got the tokens." The signature is a handle; UX must wait for the chosen commitment and then refresh cached reads.
- "API routes replace the wallet for all transactions." Routes can sponsor fees and encode policy; user authority for user-owned accounts still needs a wallet signature unless the server is the true owner.
- "SWR polling every second is the same as realtime." Tight polling hits rate limits and still lags; pair modest HTTP cache with WebSocket invalidation for hot accounts.
- "One global React context for balances across all users is efficient." Cache keys must include identity (and cluster); shared keys leak or scramble portfolio data under multi-user sessions.
FAQs
What is the one-sentence model for a Next.js Solana dApp?
Server Components and Route Handlers handle SEO, secrets, and policy; a single client provider tree owns wallet and RPC UI state; kit builds messages the wallet signs; cache and subscriptions keep displayed accounts honest under a chosen commitment.
Why App Router instead of Pages Router for new dApps?
Greenfield work should use App Router: Server Components, nested layouts, and Route Handlers map cleanly onto the server/client split Solana requires. Pages Router remains a legacy migration path only.
Where do wallet providers belong in the file tree?
In one client module (for example solana-provider.tsx) imported from the root layout, not re-mounted in every nested layout or page.
Can Server Components read the chain?
Yes for public data via server-side kit/RPC calls. They cannot access the user's wallet or private keys; pass plain serializable props into client islands for interactive UI.
When should signing stay entirely client-side?
Whenever the fee payer or authority is the end user and you are not sponsoring fees or injecting server-only policy. That is the default for DeFi, transfers, and self-custody mints.
When do I need API routes for transactions?
When you need a server fee payer, inventory or allowlist checks, Actions/Blinks endpoints, or private third-party keys during message construction.
How do I avoid window is not defined?
Never import wallet adapter entrypoints into Server Components. Keep them under "use client" modules that only load in the browser graph.
Which library should new code use: kit or web3.js v1?
@solana/kit 7.0.0 is the baseline on this site. Use web3.js v1 only while migrating legacy modules.
How should balances be formatted?
Keep lamports and token base units as integers/bigint through business logic; divide by 10**decimals only when producing a display string.
How do I keep UI fresh after a successful send?
Confirm to your target commitment, then mutate SWR/React Query keys (or rely on accountSubscribe handlers) so cached balances and account data reload.
Is NEXT_PUBLIC_RPC_URL a secret?
No. It is a public endpoint address. Proxy privileged provider headers on the server if needed, and still rate-limit client traffic.
What commitment should the dApp wait for?
Many product actions use confirmed. Use finalized before irreversible external effects. Treat processed as optimistic only.
Where should I go next in this section?
Start with dApp Integration Basics, then Providers & App Router, Client-Side Signing, Displaying On-Chain Data, API Routes for Transactions, and Caching & Realtime.
Related
- dApp Integration Basics - folder layout, env, and server/client split examples
- Providers & App Router - wallet and RPC provider tree without SSR crashes
- Client-Side Signing - build, sign, and confirm in the browser with kit
- Displaying On-Chain Data - balances, tokens, NFTs, and program state in React
- API Routes for Transactions - server-built messages, sponsorship, and Actions backends
- Caching & Realtime - SWR/React Query plus WebSocket invalidation patterns
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, Rust 1.91.1, and @solana/kit 7.0.0.