Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions blog/2026-06-12-mobile-launch/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
title: "TLSNotary on Mobile: A New App and a Path for Building Your Own"
authors: [tsukino]
description: "We just shipped TLSNotary for iOS and Android — both as a demo of selective-disclosure proofs on a phone, and as a reference implementation for developers who want to build their own TLSN-powered mobile app on top of the new @tlsn/host-react-native adapter."
---

import Figure from '@site/src/components/Figure';

A few months ago, TLSNotary was a browser extension and a protocol. Today it's also a mobile app — iOS and Android, with the same plugin gallery you know from the extension, the same selective-disclosure guarantees, and a brand-new development story for anyone who wants to ship their own TLSN-powered app.

The app itself is a **demo**: install it, browse the gallery, and produce a real proof against the public verifier. But it's also a **reference implementation**. Everything the app does on top of the TLSNotary protocol — the WebView that intercepts headers, the bottom sheet that asks for your approval, the native module that runs the Rust prover — is packaged so you can build the same stack into your own app without reading 2,000 lines of source.

<!-- truncate -->

---

## A walk through the app

The mobile app's flow mirrors the extension's, but reorganized for a phone-sized screen.

<div style={{display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '1rem', margin: '2rem 0', alignItems: 'start'}}>
<Figure src={require('./ios-plugins.png').default} caption="1. Browse plugins" />
<Figure src={require('./ios-duolingo-reveal.png').default} caption="2. Approve the plugin" />
<Figure src={require('./ios-duolingo-login.png').default} caption="3. Log in to website" />
<!-- <Figure src={require('./ios-duolingo-progress.png').default} caption="4. See the verified result" /> -->
<Figure src={require('./ios-duolingo-prove.png').default} caption="4. Generate proof" />
<Figure src={require('./ios-duolingo-approve.png').default} caption="5. Approve the reveal" />
<Figure src={require('./ios-duolingo-result.png').default} caption="6. See the verified result" />
</div>

**Browse plugins.** Each plugin is a small piece of TypeScript that knows how to prove something specific — your Spotify top artist, your bank balance above a threshold, your Twitter handle. The gallery is curated, but a developer can swap in their own registry.

**Approve the plugin.** Before any code runs, the app shows you exactly which hosts the plugin will hit, what data it will request, and asks you to choose how strictly you want to gate it: approve every reveal one-by-one, or approve everything for the session. Rejecting at this point means the plugin never executes.

**Log in to the site.** If the plugin needs an authenticated session, the app opens the target site in an in-app WebView and you sign in exactly as you normally would. The injected JavaScript watches for the specific request the plugin cares about and captures its headers — your credentials stay on the device, and nothing has been proved yet.

**Generate the proof.** Once the request is captured, the native prover takes over: it runs the multi-party TLS handshake with the verifier and replays the request to the target server, building the proof up to the moment of disclosure. A progress indicator walks through the phases — this is the heavy cryptographic work, and on a phone it's the step that takes the longest.

**Approve the reveal.** After the prover has built the protocol up to the moment of disclosure, the app stops and shows you the actual bytes that will leave your device — every range, with REVEAL or HASH badges so you can tell what's plaintext and what's a commitment. This is the same gate the extension's strict-mode flow uses, ported to a mobile-native bottom sheet.

**See the verified result.** The proof completes against the verifier, and the app shows you the value you proved (your top artist, your follower count) along with which host signed it. The full proof transcript is collapsible — most users don't need it, but it's there if you want to inspect or share it.

The whole flow runs locally except for the TLS prover's outbound connection (to the target server) and its multi-party computation handshake (with the verifier). Your session cookie never leaves the device.


## How the app uses the libraries

The mobile app sits on top of a small stack: your app, `@tlsn/host-react-native` underneath it, and `@tlsn/plugin-sdk` at the bottom — the same protocol core the browser extension runs.

<Figure
src={require('@site/diagrams/light/mobile_stack.svg').default}
darkSrc={require('@site/diagrams/dark/mobile_stack.svg').default}
alt="The mobile host-adapter stack: your Expo app builds on @tlsn/host-react-native, which builds on @tlsn/plugin-sdk."
caption="The mobile host-adapter stack — your app on top, the @tlsn/* packages below."
width={600}
/>

What that looks like in code is short. Every screen the user sees is a thin wrapper over four primitives the adapter ships. Here's how `PluginScreen.tsx` — the file that orchestrates a whole prove session — wires them together.

**1. Set up the host.** `MobilePluginHost` wraps the SDK's `HostCore` and asks you for a handful of callbacks: how to run the native prover, how to render UI, how to open a WebView. The user-approval bottom sheets are *your* component; the host calls into them when it needs a decision.

```tsx
const host = new MobilePluginHost({
onProveUntilReveal: async (request, options) => {
// Run the protocol up to compute_reveal, before the user has approved
// anything — the result includes the byte ranges that *would* be revealed.
return await proverRef.current.prepareReveal({ ...request, proverOptions: options });
},
onProveFinalize: async (sessionId, approved) => {
// After the user approves the reveal bottom sheet, finalize the proof.
return await proverRef.current.finalizeReveal(sessionId, approved);
},
onRevealApproval: ({ descriptors, approve, reject }) => {
setRevealApproval({ descriptors, approve, reject }); // shows <RevealApprovalSheet>
},
onRenderPluginUi: (windowId, domJson) => setDomJson(domJson),
onOpenWindow: async (url) => {
setWebViewUrl(url); // mounts <PluginWebView>
return { windowId, uuid: `mobile-${windowId}`, tabId: 0 };
},
onCloseWindow: () => { setWebViewUrl(null); setDomJson(null); },
});
```

**2. Mount the native prover.** `<NativeProver>` is a headless component that wraps the `tlsn-native` Expo module — a Rust port of the TLSNotary prover packaged as an iOS / Android native library (Hermes can't run the WASM build the extension uses, so we shipped one natively). The `ref` is what `MobilePluginHost` calls `prepareReveal()` and `finalizeReveal()` against.

```tsx
<NativeProver
ref={proverRef}
onReady={() => setProverReady(true)}
onError={(err) => setError(err.message)}
onProgress={handleProveProgress}
/>
```

**3. Intercept headers in a WebView.** When the plugin calls `openWindow()`, your app mounts `<PluginWebView>`, which loads the URL and runs injected JavaScript to wrap `fetch` and `XMLHttpRequest`. Every request fires `onHeaderIntercepted`, and your app routes that into the running plugin via `host.emitHeaderIntercepted()`.

```tsx
<PluginWebView
url={webViewUrl}
targetHosts={pluginConfig.urls}
onHeaderIntercepted={(header) => {
host.emitHeaderIntercepted(eventEmitter, windowId, header);
}}
/>
```

**4. Render the plugin's UI.** Plugins describe their UI as `DomJson` — a JSON tree of `div` / `button` / `text` nodes. `<PluginRenderer>` walks that tree and renders it as React Native primitives (`<View>`, `<Text>`, `<TouchableOpacity>`), translating any CSS the plugin emits to RN's `StyleSheet`. Click handlers route back to the host so the plugin can react.

```tsx
<PluginRenderer
domJson={domJson}
onPluginAction={(handlerName) => host.emitPluginAction(eventEmitter, windowId, handlerName)}
/>
```

**5. Run the plugin.** Everything above is plumbing. The actual entry point is one line:

```tsx
await host.executePlugin(pluginCode, { eventEmitter });
```

That's the whole integration. The approval sheets, the plugin gallery, the theming, the settings — those are yours to design. The TLSN-specific machinery is the snippets above plus the components that render them. The reference implementation at [`app/mobile/`](https://github.com/tlsnotary/tlsn-extension/tree/main/app/mobile) is around 600 lines on top of the adapter; you can fork it or start from scratch depending on how much UX you want to own.

---

## Build your own

There are three paths a developer can take into TLSNotary, and we ship a Claude Code skill for each of them.

**Build a plugin.** If you want to prove data from a specific API — your bank, a SaaS dashboard, anything you can hit with `fetch` — write a plugin. Plugins are platform-agnostic: the same `swissbank.plugin.ts` runs on the mobile app, the browser extension, and the CLI. The `create-plugin` skill walks you through it.

**Build your own extension.** If you want a Chrome / Firefox / Safari extension with your own branding and plugin curation, scaffold a new project on top of `@tlsn/host-extension`. You own the manifest, the popup, the gallery — the adapter brings the WindowManager, the offscreen WASM prover, the request interception, and the content-script renderer. The upstream TLSNotary extension is the canonical reference.

**Build your own mobile app.** If you want TLSN on iOS or Android, scaffold on top of `@tlsn/host-react-native`. You own the screens, the gallery, and the theming; the adapter brings the WebView, the native prover, and the renderer. The TLSN mobile app at `app/mobile/` is the canonical reference.

For all three paths, the canonical entry point is the **`tlsnotary` Claude Code skill** that lives in the monorepo at [`skills/tlsnotary/SKILL.md`](https://github.com/tlsnotary/tlsn-extension/blob/main/skills/tlsnotary/SKILL.md). Copy the `skills/` directory into your own project, point Claude Code at it, and ask something like *"add TLSNotary to my Expo app"* — Claude reads the skill, picks the right path, and walks you through scaffolding the consumer files. The skill is the canonical, auto-discoverable entry point so you don't have to remember any slash commands.

The packages and the skill are all in the monorepo at [tlsnotary/tlsn-extension](https://github.com/tlsnotary/tlsn-extension). The mobile app lives under `app/mobile/`, the adapters under `packages/host-*`, and the skills under `skills/`.

---

## What's next

The mobile app is the first ship of the new host-adapter platform — there will be a CLI cut, an Electron adapter, and more reference plugins to follow. If you're building something with TLSNotary and the tooling has a sharp edge, please file an issue on GitHub or join us on Discord. We want to hear it.

Install the app, run a proof, and let us know what you build.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added blog/2026-06-12-mobile-launch/ios-plugins.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions diagrams/dark/mobile_stack.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions diagrams/light/mobile_stack.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions diagrams/mobile_stack.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<mxfile host="65bd71144e">
<diagram name="mobile-stack" id="mobile-stack-1">
<mxGraphModel dx="854" dy="1189" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="640" pageHeight="760" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="c1" value="&lt;b style=&#39;font-size:15px&#39;&gt;Your Expo app&lt;/b&gt;&lt;br&gt;&lt;font color=&#39;#5a6675&#39;&gt;The app you ship. You own the screens (plugin gallery, runner, settings), the approval bottom sheets the user sees, and the theming.&lt;/font&gt;" style="rounded=1;arcSize=6;whiteSpace=wrap;html=1;align=left;verticalAlign=top;spacingLeft=16;spacingRight=16;spacingTop=12;spacingBottom=10;fillColor=#ffffff;strokeColor=#0969da;strokeWidth=2;dashed=1;fontColor=#1b2733;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="60" y="40" width="520" height="87" as="geometry"/>
</mxCell>
<mxCell id="c2" value="&lt;b style=&#39;font-size:15px&#39;&gt;@tlsn/host-react-native&lt;/b&gt;&lt;br&gt;&lt;font color=&#39;#5a6675&#39;&gt;The mobile adapter. Ships a WebView with injected JS that intercepts fetch / XMLHttpRequest, a headless NativeProver wrapping the tlsn-native Expo module (a native Rust port of the prover — Hermes can&#39;t run the WASM build), and a PluginRenderer that maps the plugin&#39;s DomJson tree onto React Native primitives.&lt;/font&gt;" style="rounded=1;arcSize=6;whiteSpace=wrap;html=1;align=left;verticalAlign=top;spacingLeft=16;spacingRight=16;spacingTop=12;spacingBottom=10;fillColor=#ddf4ff;strokeColor=#0969da;strokeWidth=2;fontColor=#1b2733;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="60" y="215" width="520" height="123" as="geometry"/>
</mxCell>
<mxCell id="c3" value="&lt;b style=&#39;font-size:15px&#39;&gt;@tlsn/plugin-sdk&lt;/b&gt;&lt;br&gt;&lt;font color=&#39;#5a6675&#39;&gt;The protocol core. HostCore drives the plugin lifecycle — it evaluates the plugin&#39;s JavaScript in a sandbox, runs the reactive main() loop, and exposes the capabilities a plugin uses (prove(), openWindow(), useHeaders(), …). Owns every protocol type the plugin and host exchange, and knows nothing about whether it runs on a phone, a browser, or a Node CLI.&lt;/font&gt;" style="rounded=1;arcSize=6;whiteSpace=wrap;html=1;align=left;verticalAlign=top;spacingLeft=16;spacingRight=16;spacingTop=12;spacingBottom=10;fillColor=#f3f5f8;strokeColor=#243f5f;strokeWidth=2;fontColor=#1b2733;fontSize=13;" parent="1" vertex="1">
<mxGeometry x="60" y="412" width="520" height="133" as="geometry"/>
</mxCell>
<mxCell id="bnd" value="&lt;i&gt;everything below is the @tlsn/* stack the skill scaffolds for you&lt;/i&gt;" style="text;html=1;align=center;verticalAlign=middle;fontColor=#8a93a6;fontSize=12;" parent="1" vertex="1">
<mxGeometry x="60" y="142" width="520" height="22" as="geometry"/>
</mxCell>
<mxCell id="e1" value="builds on" style="endArrow=block;endFill=1;html=1;strokeColor=#9aa7b8;strokeWidth=2;fontColor=#5a6675;fontSize=11;labelBackgroundColor=#ffffff;" parent="1" target="c2" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="320" y="128" as="sourcePoint"/>
</mxGeometry>
</mxCell>
<mxCell id="e2" value="builds on" style="endArrow=block;endFill=1;html=1;strokeColor=#9aa7b8;strokeWidth=2;fontColor=#5a6675;fontSize=11;labelBackgroundColor=#ffffff;" parent="1" source="c2" target="c3" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Loading
Loading