Skip to main content

Privy and account abstraction

Kurabase production application writes use a user-owned account, not a gateway wallet. We use Privy for authentication and wallet ownership, and choose ZeroDev for account abstraction. The core SDK deliberately does not bundle either provider. Applications that want the reviewed browser bridge can install the separate @kurabase/privy package.

Who signs and sends​

  • With an external wallet, Privy owns the login and connected-wallet flow. The connected wallet remains the account owner.
  • With social login, Privy creates and owns the embedded wallet. It is still the account owner.
  • Privy and ZeroDev send the user operation. The Kurabase gateway constructs SQL operations and attests one exact on-chain call; it is not the user signer or a general gas-paying EOA.

On networks where it is available, use ZeroDev's EIP-7702 smart-EOA path so the user keeps their existing address. Use a ZeroDev Kernel ERC-4337 account when a chain does not support 7702 or an application intentionally needs a distinct smart-account address. This selection is provider configuration, not an SDK concern.

The first OP Sepolia bridge implements the smart-EOA path only. It does not silently fall back to a distinct 4337 address: that address needs its own verified Privy identity-to-account binding before it can satisfy Kurabase's msg.sender session check. Until that binding is implemented, select Privy's direct sender instead of treating the advertised 4337 compatibility mode as a working fallback.

The transaction plan​

After gateway authentication and RLS evaluation, an application mutation returns a 202 transaction plan with { chainId, to, data, value, expiresAt }. Its calldata calls executeGatewayPlan on the instance contract. The contract verifies that:

  • the named gateway is still delegated and its grant epoch is current;
  • the attestation expires no later than its authenticated session, its account nonce is still valid, and that exact session has not been revoked;
  • its revision and ABI-encoded operation hash are exactly the proposed write;
  • the signature came from that gateway; and
  • msg.sender is the account named in the plan.

The last condition is crucial: possessing an attestation does not let a gateway submit a write as the user. The gateway signs the originating session digest into the plan, so an individual revokeSession cancels a pending plan. A revision changes on success, so the same plan cannot be replayed.

SDK integration​

The application gives the independent SDK one generic sender. For an external wallet, use Privy's normal sender: Privy owns login and the request UI, while the external wallet remains the final signer. For a Privy embedded wallet, the optional bridge upgrades the same address to a ZeroDev Kernel smart EOA and waits for the user operation receipt. This address preservation matters because Kurabase verifies msg.sender against the user session.

Install the optional bridge alongside the core SDK:

pnpm add @kurabase/js @kurabase/privy
import { useMemo } from 'react'
import { useSendTransaction, useSign7702Authorization, useWallets } from '@privy-io/react-auth'
import { optimismSepolia } from 'viem/chains'
import { createClient } from '@kurabase/js'
import { createPrivyDirectSender, createPrivyZeroDevSender } from '@kurabase/privy'

function useKurabase(gatewayUrl: string, publishableKey: string, zeroDevProjectId: string) {
const { wallets } = useWallets()
const { sendTransaction } = useSendTransaction()
const { signAuthorization } = useSign7702Authorization()
const wallet = wallets[0]

return useMemo(() => {
if (!wallet) return null
const send = wallet.walletClientType === 'privy'
? createPrivyZeroDevSender({
chain: optimismSepolia,
wallet,
signAuthorization,
zeroDevProjectId,
// Browser-safe, credential-free endpoint only. A service's private
// indexer RPC must never be injected into this application.
publicRpcUrl: 'https://sepolia.optimism.io',
sponsor: true,
})
: createPrivyDirectSender({
chain: optimismSepolia,
wallet,
sendTransaction,
})

return createClient(gatewayUrl, publishableKey, { transaction: { send } })
}, [gatewayUrl, publishableKey, sendTransaction, signAuthorization, wallet, zeroDevProjectId])
}

The provider must enable OP Sepolia and create the gas policy in its ZeroDev project before setting sponsor: true. The ZeroDev project ID is an intentionally browser-safe routing identifier; do not confuse it with a Privy app secret, a ZeroDev admin key, or a KMS session-key reference.

The resulting client is used normally:

const result = await db.from('posts').insert({ body: 'Hello' }).select()
if (result.error) throw result.error
console.log(result.transaction?.hash)

If a gateway returns an AA plan without transaction.send, the SDK returns KURA_TRANSACTION_REQUIRED rather than silently falling back to a gateway write. A fresh local Anvil stack uses the explicit direct-commit development simulator. If its Dashboard Authentication page saves a Privy App ID, its application gateway starts returning plans too; the application must still provide its own sender because the local stack does not emulate Privy or ZeroDev delivery.

Gateway credential and automation​

AA is enabled by default as a capability, not as a blank cheque. Before a gateway can automate an account, the account owner approves a ZeroDev session key whose policy is limited to the one instance contract and executeGatewayPlan, with a bounded expiry, no ETH value, and bounded call rate/value. Store that credential in a KMS/HSM boundary. It must never permit asset transfers, gateway delegation, schema management, another instance, or another user's account.

Without this grant, every plan is sent by the active Privy wallet. With it, the scoped key may ask ZeroDev to send the matching user operation through the same user account. The on-chain plan validation remains in force either way.

Provider configuration boundary​

A hosted provider has a global default Privy tenant, ZeroDev project, bundler, and paymaster policy. Each instance may select an override held in that provider's server-side configuration. Browser discovery may expose public app/client IDs, a ZeroDev project ID, and supported chains. Keep Privy verification secrets, ZeroDev administrative credentials, and session-key material out of the SDK, dashboard, instance metadata, and source control.

The shipped (draft, separate) hosted control plane stores only the resolved public { privyAppId, zeroDevProjectId, sponsorTransactions } selection and fixes the provider to ZeroDev eip7702 with kernel-4337 fallback. It accepts an owner override only before activation and rejects private-key, API-key, bundler, paymaster and session-key fields. The runner receives this public selection plus private references through its workload identity; the gateway's public /meta/v1/instance discovery documents the non-secret AA capability.

The included Anvil acceptance proves that a gateway can attest a plan while the named account, rather than the gateway, sends the final transaction. It does not claim a live Privy tenant, ZeroDev sponsor policy, KMS signer, or public-chain EIP-7702/4337 deployment. Those are production acceptance work.

GatewayPlan includes a session-digest field in this release. This changes its ABI and EIP-712 type hash, so deploy the matching contract artifact and gateway together. An older instance contract intentionally fails the production bytecode check rather than accepting a mismatched gateway.

Local project configuration​

kura start has the same public provider configuration surface in Dashboard Authentication. A local operator can set or clear privyAppId and zeroDevProjectId; the gateway updates its public discovery document without a restart. A Privy App ID enables account-owned plans for application writes. The form has no secret or credential field; all saved values are public. Privy secrets and ZeroDev sponsor/bundler/session credentials remain provider-side configuration.

Saving those IDs does not make the local gateway verify Privy browser tokens or configure the application's wallet SDK. Use ordinary EIP-1193 wallet sign-in for the local user-session path, or configure the separate Privy issuer adapter in the JavaScript SDK guide. The application-owned sender remains responsible for delivering each returned plan.