Skip to main content

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:

PrimitiveWhat it handles
zkLoginAuthentication: users sign in with Google, Apple, or other OAuth providers instead of managing keys
EnokiManaged infrastructure: handles salt storage, proof generation, and sponsored transactions behind a single API
SealEncrypted 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:

  1. Client builds the transaction and serializes it as transaction kind bytes.
  2. Server calls enoki.createSponsoredTransaction() with the kind bytes, which returns sponsored bytes and a digest.
  3. Client signs the sponsored bytes with the wallet and sends the signature back to the server, which calls enoki.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:

  1. Create a SessionKey scoped to your content package with a short time-to-live (TTL).
  2. Have the user sign the session key's personal message through dApp Kit.
  3. Build an approval transaction that calls your Move module's seal_approve_access function.
  4. 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,
});