Browser quickstart
Get a fresh single-page app from nothing to a signed-in user, using the published @timonier/browser package. Part 1 creates a project and a public app in your operator dashboard; Part 2 wires the SDK into a plain Vite SPA.
Part 1 — create a project and an app
1. Sign in to the dashboard
Open your platform's operator dashboard. Sign-up and sign-in share one screen — if you don't have an operator account yet, entering your email there creates one.
Enter your email and click Sign in, then enter the 6-digit code from the email.
The dashboard walks you straight into creating your first project.
2. Create a project
From Projects, create a new project: give it a display name and a slug, then confirm.
3. Create a public app
From the project's apps list, create a new app. Fill in:
- Display name and Slug — anything you like.
- Client type — Public. A browser SPA cannot keep a client secret, so it authenticates with PKCE alone.
- Redirect URI 1 —
http://localhost:5173/callback. Vite's dev server defaults to port5173, so nothing later in this guide needs a port flag. - Signup enabled — leave this checked (it's checked by default). If you uncheck it, the sign-up step later in this guide is rejected by the platform.
Confirm, then save your credentials.
4. Get your four values
Open the app's Credentials screen. You'll need four values for Part 2:
- The Client ID shown on the Credentials screen.
- Your project slug.
- Your app slug.
- Your auth data-plane origin (the same host the dashboard's Integration tab derives a scheme for).
Part 2 — the SPA
1. Scaffold the app
npm create vite@latest app -- --template vanilla-ts
This scaffolds a plain TypeScript project in app/, no framework.
2. Install the SDK
cd app
echo "@timonier:registry=https://npm.registry.timonier.eu/" >> .npmrc
npm install @timonier/browser3. Configure
cat > .env.local <<'EOF'
VITE_TIMONIER_BASE_URL=<your data-plane origin>
VITE_TIMONIER_PROJECT_SLUG=<your project slug>
VITE_TIMONIER_APP_SLUG=<your app slug>
VITE_TIMONIER_CLIENT_ID=<your app's Client ID>
EOF
Use the four values from Part 1, step 4.
This step's fence, and the
src/main.tsfence in the next step, are extracted verbatim by the clean-room gate that guards this guide. If you edit either, keep the gate passing — it's the guide's own regression test, not a copy of it.
4. Write the app
Replace src/main.ts with the complete file below. It renders into the scaffolded index.html's existing #app div — nothing there needs editing.
import from "@timonier/browser";
function requireEnv(name: string): string
const value = importmeta
if!
throw new Error`Missing required env var: ${name}`
return value as string
const baseUrl = requireEnv("VITE_TIMONIER_BASE_URL");
const projectSlug = requireEnv("VITE_TIMONIER_PROJECT_SLUG");
const appSlug = requireEnv("VITE_TIMONIER_APP_SLUG");
const client = createClient({
baseUrl,
projectSlug,
appSlug,
requireEnv"VITE_TIMONIER_CLIENT_ID"
});
const app = document.querySelector<HTMLDivElement>("#app")!;
function renderSignedIn(email: string): void
= `
<p>Signed in as <span id="user-email"></span></p>
<button id="clear-session">Clear local session</button>
`
// textContent, not innerHTML: email is server data, rendered into someone
// else's page by every reader who copies this guide verbatim.
querySelectorHTMLSpanElement"#user-email"! =
document
querySelectorHTMLButtonElement"#clear-session"!
addEventListener"click" =>
clearSession
renderSignedOut
function renderSignedOut(): void
= `<button id="sign-in">Sign in</button>`
document
querySelectorHTMLButtonElement"#sign-in"!
addEventListener"click" =>
signIn + "/callback"
async function refresh(): Promise<void
try
const user = awaitgetUser
renderSignedIn
catch
if instanceof UnauthenticatedError
renderSignedOut
else
throw
if (location.pathname === "/callback"
awaithandleCallback
replaceStatenull "" "/"
await refresh();
handleCallback() returns nothing — it stores the token internally — so both the callback path and every normal page load converge on the same refresh() call, which calls client.getUser() (minting a token from the session cookie if needed) to fetch the signed-in user's profile and renders accordingly. getUser() throws UnauthenticatedError exactly like getAccessToken() when there is no session.
Clear local session does exactly what it says: it drops the in-memory access token and clears transient sign-in state, but it does not end your session on the platform. Reload the page afterwards and you'll see Signed in as your email again —
refresh()re-mints a token from the still-live server-side session cookie and re-fetches your profile. There is no sign-out in this guide; ending the platform session itself is a separate, not-yet-covered operation.
5. Run it
npm run dev
Visit http://localhost:5173. You should see a Sign in button.
6. Sign in
Click Sign in. You're taken to the platform's hosted sign-in screen. Enter your email, then the 6-digit code from your inbox. You're returned to http://localhost:5173/callback, which immediately redirects to / showing Signed in as your email.
7. Build and preview
npm run build
npm run preview -- --port 5173
Visit http://localhost:5173 again. The built, served app behaves the same as the dev server: Signed in as your email, because the session cookie from step 6 is still live.
API reference
This guide covers sign-in, callback handling, token retrieval, profile lookup, and local session clearing — the whole surface @timonier/browser exposes:
signIn({ redirectUri })— starts the PKCE flow and navigates to the hosted sign-in screen.handleCallback()— completes the flow on your redirect route. Returns nothing; it stores the token internally.getAccessToken()— returns the current access token, minting one from the session cookie when needed. ThrowsUnauthenticatedErrorwhen there is no session.getUser()— returns the signed-in user's profile ({ sub, email, name, emailVerified }) by callingGET /userinfowith the current access token. Caches nothing. ThrowsUnauthenticatedErrorwhen there is no session.clearSession()— drops the in-memory token and transient sign-in state. Local only.
The package ships TypeScript declarations, so your editor has the full types once it is installed.