Passkeys Prerequisites
Passkeys are a great way to connect users with your dapp without the friction of seed phrases or browser extensions. Learn more on the smart wallets guide.
For this tutorial we'll be using Smart Account Kit, which handles the WebAuthn ceremony and deploys an audited OpenZeppelin Smart Account for each of your users.
Here's the good news: this page used to be a long one. The previous version of this tutorial had you provision a Launchtube token and compile and deploy a Zephyr program to Mercury before you could write a single line of passkey code. Both of those chores are gone. You have exactly one prerequisite now: an API key for the relayer.
OpenZeppelin Relayer
Earlier versions of this tutorial used Launchtube as a paymaster service. Launchtube has since been retired (the stellar/launchtube repository is archived and the hosted service is offline), and its successor is the OpenZeppelin Relayer running the Stellar Channels plugin. The relayer submits transactions on your users' behalf through a pool of managed channel accounts, so it takes care of fees, sequence numbers, and source accounts. If you're coming from the EVM world, this is the rough equivalent of a paymaster.
We'll use the hosted Testnet relayer at https://channels.openzeppelin.com/testnet. To authenticate with it, you'll need an API key.
-
Visit https://channels.openzeppelin.com/testnet/gen to generate a Testnet key.
-
Copy the key into your
.envfile:PRIVATE_RELAYER_BASE_URL="https://channels.openzeppelin.com/testnet"PRIVATE_RELAYER_API_KEY=<paste-your-testnet-api-key>
That's the whole prerequisite. Told you it was shorter.
The PRIVATE_ and PUBLIC_ prefixes are a SvelteKit convention. Variables prefixed PRIVATE_ are only readable from server-side code via $env/static/private, so the relayer key never ships to the browser. Variables prefixed PUBLIC_ are inlined into the client bundle via $env/static/public.
Calls to the Channels relayer authenticate with the API key in an Authorization: Bearer header. Put that call in the browser and you've handed the key to anyone who opens their network tab, and they can then spend your credits on whatever they like. This tutorial keeps the key server-side by routing every submission through a same-origin /api/send route, which we'll build on the next page.
What you no longer have to set up
Two things that used to live on this page are now handled for you. It's worth knowing who is doing that work, though, because "I don't configure it" is not the same as "it isn't happening."
Funding new wallets
Friendbot still only funds classic G... accounts, so it can't top up a C... contract address directly. The old workaround was a funder account of your own: you'd generate a keypair, park some Testnet XLM in it, and run an /api/fund endpoint that transferred lumens to each new smart wallet.
Smart Account Kit does this dance internally now. Ask it to fund a wallet and it creates a temporary account, funds that from Friendbot, and transfers the XLM on to your user's contract address. So there's no funder keypair to generate and no secret key in your .env. We'll see the two places this shows up (at signup, and behind a "Fund Wallet" button) on the frontend page.
The reverse-lookup indexer
When a returning user taps their passkey, your dapp gets a credential ID back from the authenticator. It still needs to turn that credential ID into the smart account contract address the credential was added to, and that reverse lookup still requires indexed network data. That requirement did not go away with Launchtube.
What changed is that you no longer build the indexer yourself. Smart Account Kit ships an indexer client that defaults to Mercury's hosted smart-account-indexer for Testnet and Mainnet. Its read endpoints are public, so there's no JWT to fetch and no Zephyr program to compile and deploy. You can point indexerUrl at any wire-compatible provider, or pass indexerUrl: false to switch discovery off entirely.
This is a boundary that's easy to blur when migrating an older passkey app, so to be explicit about it: the OpenZeppelin Relayer replaced Launchtube's submission and fee-sponsorship role. It did not replace Mercury's indexing and reverse-lookup role. If you rip out your Launchtube code and your indexer configuration in the same commit, you'll break returning-user login on any device that hasn't seen that passkey before.
The other half of this story is local: we'll configure the kit with IndexedDBStorage so a connected session survives a page reload. That's a per-browser cache sitting in front of the indexer, not a substitute for it.
Troubleshooting
Things that can trip you up while getting this wired up:
- The relayer returns a 401 or a 403. Generate a fresh key at https://channels.openzeppelin.com/testnet/gen. Testnet keys are free to re-issue, so there's no harm in doing this whenever you're suspicious of one.
- Your
PRIVATE_*variables come back undefined. SvelteKit reads.envwhen the dev server starts. If you edited the file whilepnpm devwas running, restart it. - Login can't find a wallet you know you created. You're probably in a different browser or profile than the one you signed up in, with an empty IndexedDB, which means you're exercising the indexer path. Give it a few ledgers to catch up on a freshly deployed wallet, and check your browser console for a failed request to the indexer.
If you get stuck, drop a question in the #passkeys channel on the Stellar Developer Discord. There's usually somebody around who's ready and willing to help out!