Core Concepts
The mental model behind Oviato — passkeys, sessions, popup flows, chain scoping, the nonce tracker.
You can use Oviato without reading this page — the SDK handles everything. But when something surprises you later, come back here: this is where the moving parts are named.
The data model
Oviato has four things a developer interacts with.
App. Your product. Created in the Developer Dashboard — gives you an appId, domains, theme, supported networks. The SDK reads your app's config at initialize() time.
User. A human with a passkey. Identified by a username (which maps to an OVI.ID). A user can connect to many apps; their wallet addresses are deterministic per (user, app, network).
Session. A signed JWT + the user's public state (address, network, hdkeys). Created on login and kept in-memory + localStorage by the SDK. Scoped to one network at a time — switching networks re-issues the JWT.
Bridge. The popup/iframe UI at connect.oviato.com where the user approves sign requests. Your dapp never touches private keys; signatures happen inside the bridge via WebAuthn.
The passkey flow
WebAuthn passkeys are how users sign in and sign transactions — no passwords, no seed phrases.
First-time registration. User types a username, the SDK calls register(). The browser prompts for a passkey (Face ID / Touch ID / security key). A cryptographic keypair is generated + stored in the user's device keychain. The public key is uploaded to Oviato; the private half never leaves the device.
Login on the same device. login() opens a WebAuthn prompt. The device proves possession of the private key by signing a server-issued challenge. SDK receives a JWT + hd-keys, session starts.
Login on a new device. Passkeys sync across a user's Apple/Google/Microsoft ecosystem automatically. A user who registered on iPhone can log in on their Mac without extra setup.
Signing a transaction. The dapp calls sendTransaction / writeContract / etc. A popup opens showing the full transaction details. User approves with their passkey — WebAuthn signs on the device, the bridge completes the on-chain signature server-side, and the tx is broadcast.
The dapp never holds a private key or sees the passkey. All signing flows through the bridge UI, gated by a biometric/security-key tap.
Sessions are chain-scoped
A session's JWT is issued for one network — eth, base, bitcoin, etc. When a user switches networks you must re-issue the JWT.
import { switchNetwork, Network } from '@oviato/connect';
const r = await switchNetwork(Network.BASEMAINNET);
if (r.status === 'success') {
// SDK's session is now Base-scoped. Every subsequent call
// (reads, writes, balance) targets Base until you switch again.
}Switching networks requires a WebAuthn assertion — the user will see one passkey prompt to re-sign the session. This is deliberate: it prevents a stolen JWT from being silently repointed to a different chain.
Under the hood, switchNetwork calls refreshSession + re-opens the bridge for a one-tap signature.
Popup vs iframe
Write operations (sign a message, send a tx, switch a network) always show an approval UI. The SDK picks between two modes:
- Iframe (default) — overlays a modal in your page. Better UX, works on mobile, same-domain-feel. Used on production HTTPS.
- Popup — opens a new browser window. Only used when iframe can't work:
- Localhost / non-HTTPS — iframes have security restrictions in insecure contexts, so the SDK auto-falls-back to popup.
- Safari / iOS with WebAuthn (passkey create/login) — Safari has long-standing bugs around WebAuthn in iframes; forced to popup for those ops only.
await evm.sendTransaction({ to, value, method: 'iframe' }); // force iframe
await evm.sendTransaction({ to, value, method: 'popup' }); // force popupIn production, expect iframe everywhere. Popup is a localhost-dev concession and a Safari-WebAuthn concession — you typically don't need to think about it.
The response envelope
Every write method resolves to a discriminated envelope, never a raw value.
type Result =
| { status: 'success'; type: 'sendTransactionSuccess'; data: { hash, network, explorerUrl } }
| { status: 'error'; message: string };Narrow on status + type before reading .data:
const r = await evm.sendTransaction({ to, value });
if (r.status === 'success' && r.type === 'sendTransactionSuccess') {
console.log('Hash:', r.data.hash);
} else if (r.status === 'error') {
console.error(r.message);
}This is different from ethers/viem, which return a raw hash and throw on error. See Migrating from viem for a one-line adapter if you prefer that style.
The nonce tracker
EVM transactions need a monotonically-increasing nonce. A dapp that fires three sendTransaction calls in 100 ms would collide if they all read eth_getTransactionCount('pending') at the same time — the chain hasn't caught up yet.
Oviato runs a server-side nonce tracker per (address, chain). Rapid-fire calls serialize correctly without you having to manage anything.
// These three calls all get unique, sequential nonces automatically.
const [r1, r2, r3] = await Promise.all([
evm.sendTransaction({ to, value: 10n ** 15n }),
evm.sendTransaction({ to, value: 2n * 10n ** 15n }),
evm.sendTransaction({ to, value: 3n * 10n ** 15n }),
]);If a broadcast fails pre-mempool (replacement underpriced, nonce too low), the tracker automatically releases the nonce for reuse. You get a
structured error with a retryAfter hint — no manual recovery needed.
Protected vs read-only methods
Some methods require a valid Bearer JWT; others are public reads.
Protected (JWT required):
connect_prepare+connect_preparePsbt(UTXO writes)connect_prepareEvm+connect_broadcastEvm(EVM writes)refreshSession
Read-only (no JWT needed):
- All
evm.*reads (contract, chain, fees, tokens, NFTs) rpc.getBalance(), balance history, username resolutionx402payment flows
Rate limits apply to protected methods per (app, address). Reads have looser app-level limits.
Server-side components you don't see
The SDK looks simple because most of the complexity lives behind it:
- Bridge worker (
connect.oviato.com) — hosts the popup/iframe UI, holds the WebAuthn flow, signs transactions server-side. - RPC API (
rpc.oviato.com) — JSON-RPC router. Handles auth, rate limits, nonce tracking, activity logging. Routes on-chain reads to Chainstack. - Chainstack — our RPC provider for EVM chains. Behind the API; you never talk to it directly.
- D1 + KV — Cloudflare storage for users, app config, nonce state, token cache, activity logs.
You don't need to understand these to ship. They matter when you hit an edge case (rate-limit error, wedged nonce, cache miss) and want to know what's going on.
Next steps
- Quickstart — build a working sign-in + transfer in 10 minutes
- Chain-Agnostic → Overview — methods that work across BTC + EVM
- EVM → Overview — the
evm.*namespace, popup flow, envelope pattern