11Docs
Auth
Bounded handles sign-in for your app. Users can sign in with email, social accounts, or supported wallets. Your developer account controls the app separately from its users.
The two identity systems
| Who | What it is | Where it shows up | |
|---|---|---|---|
| Dev identity | you / your agent | normally a Bounded web session selected by bounded init | owns apps; the actor bounded deploy / data run as |
| End-user auth | your app's users | Bounded Auth hosted login, anonymous guests, or a connected wallet (Phantom) | @user.id, @user.address, and @user.email in policy rules |
Developer identity
bounded init reuses or refreshes your saved web session, opens hosted browser login when needed, and writes public bounded.json. Tokens stay outside the project. Local signing profiles and BOUNDED_PRIVATE_KEY are advanced explicit alternatives for key-owned or automated workflows.
bounded whoami # developer identity, environment, account sourceUse bounded account use client-a to run this project under ~/.bounded/accounts/client-a/credentials, bounded account use --project for <project>/.bounded/credentials, or override with BOUNDED_PRIVATE_KEY for CI. Never reuse a human’s keypair for an autonomous agent unless that is explicitly intended.
Advanced local keys & teams
Normal projects can skip local keys and linking. If you deliberately selected a local signing key for a legacy or automated workflow, you can link it for recovery. Sharing teammates is available to web-account and key-owned apps:
bounded linkruns an OAuth-style device flow: the CLI prints a verify URL + code, you approve in a browser signed in as your account — email/social or a Solana wallet — and the CLI records the linkage. Headless agents can usebounded link --email you@example.com: the CLI emails an OTP, reads it from stdin, and approves the same fingerprint checked device flow. After linking, your keypair address and your account’s wallet become admin-collaborators on each other’s apps. Your keypair keeps signing for everything, linking adds an association, it never replaces or rolls your key.bounded share <wallet|email> --role developer|admin|viewer|billing --app-id <id>adds a collaborator. A wallet is added directly. An email is resolved to that person’s canonical wallet, an auto-provisioned embedded wallet, so the invitee needs no wallet of their own.policyis a legacy alias fordeveloper. Owner only.
Sign users in
Use @bounded-sh/client for app sign-in. The default Turnkey flow gives supported email and social users an embedded Solana wallet.
import { init, openBoundedWidget, getCurrentUser, useAuth } from "@bounded-sh/client";
await init({ appId: "<appId>" });
// In a click handler, open the default email and social sign-in widget.
await openBoundedWidget({ methods: ["email", "google"] });
const current = getCurrentUser();
// React:
function AuthButton() {
const { user, logout, loading } = useAuth();
if (loading) return <span>Loading account...</span>;
return user
? <button onClick={logout}>Sign out</button>
: <button onClick={() => openBoundedWidget({ methods: ["email", "google"] })}>Sign in</button>;
}Use @user.id for ordinary record ownership and membership. Use @user.address when a rule needs the user's wallet. Guest users can sign in with signInAnonymously().
For an existing external wallet, enable walletLogin: true in init(). Also declare { "auth": { "wallets": true } } in the app policy. The wallet signs the login challenge. Without the policy opt-in, the issuer returns 403 wallet_login_disabled.
Use getCurrentUser() to read the current user. React components can use useAuth(). Wait for authentication to finish before issuing writes that require a user.
Embedded Solana wallets
Wallets are Turnkey wallets. The default email and social flow creates the wallet before login completes. You do not need an auth.wallets block for that default flow.
The SDK supports signMessage, signTransaction, and signAndSubmitTransaction for supported signing sessions. Use the SDK's signing flow so the user can approve the action.
Guest sessions do not receive an embedded wallet. Legacy hosted-login modes and explicit wallet opt-outs can also leave @user.address empty.
See Solana for transaction submission, network settings, and onchain access rules.
Payments belong to the app, not the auth page
Wallet identity can support a payment flow, but it is not the payment contract. Bounded apps can use direct USDC on Solana, or a provider integrated in a Function. Each path has different custody, fees, lifecycle, and reconciliation semantics.
Read Payments for direct providers, payments.acceptCrypto, direct-provider integrations, and the rule that app value is granted only after trusted settlement.
How the user reaches your rules: @user.id, @user.address, @user.email
Every authenticated request carries a session token bound to the app (the App-Id audience). The realtime worker verifies it and exposes the end-user to the policy as three variables:
| Variable | What it is | Use it for |
|---|---|---|
@user.id | The universal, stable identity. ALWAYS present for an authenticated user (null otherwise). For wallet (Phantom) logins it equals the wallet address; for Bounded Auth logins it is the account identity. | Ownership & membership. This is the right key for "who owns this". |
@user.address | The user’s REAL wallet address. Present for wallet (Phantom) and server-keypair sessions; null for Bounded Auth sessions unless a wallet is linked. | Onchain operations only (transfers, fee payer). Onchain rules may use ONLY @user.address. @user.id and @user.email are rejected there. |
@user.email | The verified, lowercased email for email/OAuth accounts; null for wallet and phone-only text sessions. | Email-gating. Store compared email fields lowercased so equality matches. |
Ownership is the hinge of most auth rules. Key it on @user.id so it works for every login method (a wallet user and an email user are both covered, and an email user with no wallet still owns their data):
"create": "@user.id != null && @newData.owner == @user.id"The leading @user.id != null is mandatory, without it an unauthenticated caller writing owner: null satisfies null == null. The prover hands you that exact counterexample if you forget it (Verification).
Server-side identity
On a server, the same kind of keypair drives @bounded-sh/server. Server-signed writes arrive with the keypair’s address as @user.address (== @user.id), so server logic is just another authenticated actor the rules judge, give the vault key exactly the access its rules require, no more.
import { createWalletClient } from "@bounded-sh/server";
const vault = await createWalletClient({ keypair: process.env.VAULT_KEY! }); // base58 or JSON array
vault.address; // the signer this app acts as, arrives in rules as @user.address (== @user.id)Solana transactions
Onchain writes need a wallet signature and a configured RPC endpoint. Fee sponsorship depends on the transaction path. Do not assume every wallet operation is free.
See Solana for setup and the deployed invariant coverage. Mainnet policy updates require authorization from the app's permanent owner wallet.