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.senderis 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.