SDK
Install @doshi/sdk and mount it in your web page or app. Nothing loads from us at runtime but your members' data.
The SDK is Doshi’s Learn screens as a package. A page of yours, inside your iOS or Android
app or on the web, calls mount with an element and a way to get a token, and Doshi
draws inside that element, in a shadow root of its own. The code is the version you
installed, and it changes only when you upgrade.
What you build: one endpoint on your backend, and a mount call on your page. Pick
where that page runs, and the steps below show only that case.
Members need a supported browser.
1. Set up in your console
Section titled “1. Set up in your console”Your backend vouches for a member by signing a short-lived JWT with a key only it holds. Your Doshi console makes that key for you.
- Sign in to your console, and switch it to Test in its header. Getting access has its address.
- In Settings, on the Integration tab, press Add key and choose Generate a key.
- Copy the four
DOSHI_*variables the console shows you into your backend’s secret store. They are shown this once:DOSHI_ISS, the issuer Doshi assigns you;DOSHI_AUD, our exchange endpoint;DOSHI_KID, the key’skid; andDOSHI_PRIVATE_KEY.
To keep the private key in your HSM or KMS instead, add its public key or a JWKS URL; see Adding a key.
No access yet? See Access and invites.
2. Allow your pages
Section titled “2. Allow your pages”Our API answers requests only from the origins your console allows.
Allow the origin your app serves the SDK page from. It must be an https:// virtual host,
such as Android’s https://appassets.androidplatform.net. The console takes no app
scheme, so email hello@doshi.app before you build on one.
Origin: null is never allowed. A page loaded from file: sends Origin: null,
and so does every such page on every device, so allowing it would allow all of them.
Serve the page from an origin of your own.
Allow every origin whose pages mount the SDK.
Add each as a bare origin, such as https://app.acme.example, at the console’s setup step
Allow the pages that show Doshi, or after your first exchange on its Allowed
origins tab. What the console accepts is in Allowed origins.
3. Add a token endpoint
Section titled “3. Add a token endpoint”Add one endpoint to your backend, such as POST /api/doshi-token. For the customer your
own session has already authenticated, it signs an assertion and exchanges it for a
member token. Copy sign-assertion.ts from Sign an assertion: it is tested
against the rules the exchange applies. With it, the endpoint’s core is:
import { configFromEnv, fetchMemberToken } from "./sign-assertion";
// DOSHI_ISS, DOSHI_AUD, DOSHI_KID and DOSHI_PRIVATE_KEY, from your secret store.const config = configFromEnv(process.env);
// In your "give my page a coach token" endpoint, for the customer that endpoint// has already authenticated:const token = await fetchMemberToken(config, customer.id, { display: { name: customer.name, customer_since: customer.since },});// -> { access_token: "...", token_type: "Bearer", expires_in: 900 }Set the four DOSHI_* variables to the values your console showed you in step 1. Return
access_token and expires_in to your page, and nothing else.
The endpoint must authenticate the caller as your own session holder, and sign for that customer only. It is where your session becomes a Doshi session, so nothing else may reach it.
Call it with your Test values. A 400 names its cause in the error table.
4. Install the package
Section titled “4. Install the package”The package is @doshi/sdk on npm. Each release comes with the evidence
to check it, so verify it, then install that version:
npm install @doshi/sdk@0.3.2The package is one JavaScript file with its declarations, and it installs no other package.
5. Mount it
Section titled “5. Mount it”This is the whole host side. It is the source of our own example host page, imported into this page rather than copied, so it is the code our end-to-end tests drive.
// A bank's page with Doshi in it: mount into one element, and unmount on logout.
import { type Mounted, mount } from "@doshi/sdk";import { getToken } from "./host";import { LOCAL_STACK } from "./local-stack";
function byId(id: string): HTMLElement { const element = document.getElementById(id); if (!element) throw new Error(`the page has no #${id}`); return element;}
const slot = byId("doshi");const status = byId("status");const session = byId("session");
let mounted: Mounted | null = null;let readyCount = 0;
function signIn(): void { status.textContent = "loading"; mounted = mount(slot, { getToken, urls: LOCAL_STACK, onReady: () => { readyCount++; status.textContent = "ready"; status.dataset.readyCount = String(readyCount); }, onError: ({ code }) => { status.textContent = code === "auth_expired" ? "session ended" : "unavailable"; }, }); session.textContent = "Log out";}
function logOut(): void { mounted?.unmount(); mounted = null; status.textContent = "logged out"; session.textContent = "Log in";}
session.addEventListener("click", () => (mounted ? logOut() : signIn()));signIn();Two lines are there only because the example runs against a local copy of Doshi: the
LOCAL_STACK import and urls: LOCAL_STACK. Leave both out. Your page needs
urls only to proxy our traffic.
getToken fetches the member’s token from the endpoint of step 3. The example’s backend
answers { token }. Yours answers with whatever your page reads, such as the
access_token from the token exchange.
// The member's token, from the page's own backend. Yours calls your own// token endpoint: the one that signs an assertion for the signed-in// customer and exchanges it.
export async function getToken(): Promise<string> { const res = await fetch("/token", { method: "POST" }); if (!res.ok) throw new Error(`token endpoint: ${res.status} ${await res.text()}`); const { token } = (await res.json()) as { token: string }; return token;}- Answer every call with a fresh token. Doshi calls
getTokenbefore its first screen, then about 20 seconds before each token expires. It caches the token itself, so you should not. See the contract. - Reject when the member has no session. Doshi then blanks its content and calls
onErrorwithauth_expired, once. See Expiry. - Size the element yourself. The SDK fills the element it is mounted in, and your page’s styles do not reach inside the shadow root.
The SDK is JavaScript, so in an app it runs in a webview:
- Ship the host page inside your app, and load it in the webview from the origin you allowed in step 2.
- Give
getTokento your native code: it asks your app for a token from your endpoint, and rejects when the member has no session. - Pass
onOpenLink, and open each URL it hands you in the system browser rather than in your webview. Today’s Learn screens have no outside links, so it is not called yet; passing it now means future links reach your native code. - On
auth_expired, sign the member in again and mount afresh.
The host page above is the whole of it.
6. Allow our hosts in your Content Security Policy
Section titled “6. Allow our hosts in your Content Security Policy”If your page sends a Content-Security-Policy, merge these sources into it, as
Content Security Policy explains:
connect-src https://core.doshi.app https://gateway.doshi.app;img-src https://core.doshi.app;font-src data: https://core.doshi.app;The SDK’s default fonts are inlined as data: URLs in one <style> element it adds to your
<head>. Under a style-src without 'unsafe-inline', Doshi draws in fallback fonts. Font
files you send us for your brand load from the data host.
See Content Security Policy.
7. Check it in Test
Section titled “7. Check it in Test”8. Go live
Section titled “8. Go live”Switch your console to Live in its header, and repeat steps 1 and 2 there: a Live key, and its four
DOSHI_* variables in your backend’s secret store, then the origins of your live pages. Live has its own issuer, and should
have its own key; see Yours. For Live we recommend
your own public key over a generated one.
Then deploy your backend with the Live variables. No host changes: the exchange
URL, and with it aud, is the same in both environments.
Options
Section titled “Options”mount(element, options) draws Doshi inside element and returns { unmount }.
| Option | Required | What it is |
|---|---|---|
getToken |
yes | () => Promise<string>: the member’s token, from your backend. See Hand over the token. |
start |
no | Where Doshi opens: { view:, { view: or { view:. See Parameters. |
locale |
no | A BCP 47 tag such as nl-. See Parameters. |
urls |
no | { data, api }: base URLs that replace our hosts, for a page that proxies our traffic through its own origin. |
onReady |
no | () => void, called once, when the first screen has rendered. See Events. |
onError |
no | ({ code, during }) => void, called once per code. See Events. |
onOpenLink |
no | (url) => void, handed every outside link the member follows. Learn has none today. |
A misspelt option throws. mount throws a TypeError on any key it does not have,
and on a start it cannot read. An option that was silently ignored would be a mount
that behaves differently from what you wrote.
There is no environment or client option. The token says both: the environment it was minted in, Live or Test, and your client.
Outside links. No screen the SDK ships has an outside link today, so onOpenLink
is for links to come. Without onOpenLink, the SDK opens an outside link in a new window
with noopener. With it, the SDK opens nothing and hands you the URL instead, which is
what a native shell needs. It never navigates your page either way.
One mount at a time. A second mount throws until the first is unmounted.
Expiry
Section titled “Expiry”When getToken rejects, Doshi blanks its content and calls onError with
auth_expired, once. See Hand over the token.
Logout
Section titled “Logout”Call unmount(). It removes everything the mount drew and forgets the member’s token.
There is nothing else to call. See Hand over the token.