Consumer App Cookbook
Consumer applications targeting non-crypto users need to eliminate every point of friction in the onboarding flow. The Sui stack provides three primitives that, when combined, create an invisible-crypto experience:
| Primitive | What it handles |
|---|---|
| zkLogin | Authentication: users sign in with Google, Apple, or other OAuth providers instead of managing keys |
| Enoki | Managed infrastructure: handles salt storage, proof generation, and sponsored transactions behind a single API |
| Seal | Encrypted storage: lets you gate access to content or data based on onchain conditions without exposing it publicly |
Pattern 1: Zero-friction sign-up
Goal: User taps Sign in with Google and lands in the app with a funded address, ready to transact.
Stack: zkLogin (auth) and Enoki (salt, proof, sponsored gas)
Set up dApp Kit with Enoki
Register Enoki wallets so they appear in the standard wallet connection UI.
import { createDAppKit } from '@mysten/dapp-kit-react';
import { registerEnokiWallets } from '@mysten/enoki';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const GRPC_URLS = {
mainnet: 'https://fullnode.mainnet.sui.io:443',
testnet: 'https://fullnode.testnet.sui.io:443',
} as const;
export const dAppKit = createDAppKit({
networks: ['mainnet', 'testnet'],
defaultNetwork: 'testnet',
createClient(network) {
return new SuiGrpcClient({ network, baseUrl: GRPC_URLS[network] });
},
});
registerEnokiWallets({
apiKey: process.env.NEXT_PUBLIC_ENOKI_API_KEY!,
providers: {
google: {
clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
},
},
clients: dAppKit.networks.map((network) => dAppKit.getClient(network)),
getCurrentNetwork: () => dAppKit.stores.$currentNetwork.get(),
});
Add the connect button
The ConnectButton renders a Sign in with Google flow through Enoki. After sign-in, useCurrentAccount returns the user's zkLogin address.
import { useCurrentAccount } from '@mysten/dapp-kit-react';
import { ConnectButton } from '@mysten/dapp-kit-react/ui';
function App() {
const account = useCurrentAccount();
return (
<div>
<ConnectButton />
{account && <p>Welcome! Your address: {account.address}</p>}
</div>
);
}
Pattern 2: Gasless transactions with Enoki
Goal: Users perform onchain actions without holding SUI or understanding gas.
Stack: Enoki sponsored transactions and zkLogin
The Enoki sponsorship flow has 3 steps:
- Client builds the transaction and serializes it as transaction kind bytes.
- Server calls
enoki.createSponsoredTransaction()with the kind bytes, which returns sponsoredbytesand adigest. - Client signs the sponsored
byteswith the wallet and sends the signature back to the server, which callsenoki.executeSponsoredTransaction({ digest, signature }).
See the zkLogin for DeFi guide for a complete example of this flow with DeepBook swaps.
Pattern 3: Gated content with Seal
Goal: Gate access to premium content, features, or data based on NFT ownership or other onchain conditions, while the user's identity stays private.
Stack: zkLogin (identity), Seal (encryption), and Enoki (gas sponsorship)
Seal uses session keys to authorize decryption without requiring a wallet signature for every request. The flow:
- Create a
SessionKeyscoped to your content package with a short time-to-live (TTL). - Have the user sign the session key's personal message through dApp Kit.
- Build an approval transaction that calls your Move module's
seal_approve_accessfunction. - Pass the approval transaction bytes and session key to
sealClient.decrypt().
import { SealClient, SessionKey } from '@mysten/seal';
import { Transaction } from '@mysten/sui/transactions';
import { fromHex } from '@mysten/sui/utils';
const sealClient = new SealClient({
suiClient,
serverConfigs: KEY_SERVER_CONFIGS,
verifyKeyServers: true,
});
const sessionKey = await SessionKey.create({
address: account.address,
packageId: CONTENT_PACKAGE_ID,
ttlMin: 10,
suiClient,
});
const { signature } = await dAppKit.signPersonalMessage({
message: sessionKey.getPersonalMessage(),
});
await sessionKey.setPersonalMessageSignature(signature);
const approvalTx = new Transaction();
approvalTx.setSender(account.address);
approvalTx.moveCall({
target: `${CONTENT_PACKAGE_ID}::content::seal_approve_access`,
arguments: [
approvalTx.pure.vector('u8', fromHex(encryptionId)),
approvalTx.object(contentObjectId),
approvalTx.object(accessTokenId),
],
});
const txBytes = await approvalTx.build({
client: suiClient,
onlyTransactionKind: true,
});
const decrypted = await sealClient.decrypt({
data: encryptedObject,
sessionKey,
txBytes,
});
Related resources
- zkLogin Integration Guide: implement zkLogin from scratch
- Enoki documentation: managed zkLogin and sponsored transactions
- Seal documentation: encrypted storage and access control
- zkLogin Wallets: wallet implementations using zkLogin
- Sponsored Transactions: how gas sponsorship works on Sui