JavaScript Documentation
Sell your JavaScript web app from your own website — one platform for purchasing (UseThatApp is the merchant of record: hosted buy links plus a live pricing API), licensing (live entitlements), and authentication ("Sign in with UseThatApp" over OAuth 2.0 / OpenID Connect). A buyer who purchases on your site lands back in your app signed in, with the entitlement already live — sign-in plus the live plan is how you gate features. The usethatapp npm package is a confidential, server-side client — it holds your client secret and validates ID tokens, so it runs on your Node server, never in the browser. Each framework guide below uses the tested example from the package — copy, paste, and ship.
Read the protocol first
The OpenID Connect page covers the model these guides build on — the authorization-code flow, the pairwise sub, and the entitlement API. The framework pages here are the concrete wiring.
This guide covers the Hosted sign-in path
It has UseThatApp run login, so buyers return signed in and the same session answers what they bought — the Hosted sign-in add-on. If your app already has its own accounts, you do not need any of the sign-in wiring below: keep your login and verify purchases with a license key your server validates in one call, included in the per-sale fee.
What the SDK does
A single OIDC login gives you both identity (a privacy-preserving, per-app sub) and access to the entitlement API (the user's live plan). Three phases:
- Sign in. Start a login with
beginLogin(), send the user to UseThatApp to authenticate, and finish in your callback withcompleteLogin(). You get aUtaSessioncarrying the user's pairwisesuband OAuth tokens. - Read the plan. Call
getEntitlement(accessToken)whenever you gate features. Always authoritative — a canceled license stops being entitled immediately. - Offer the upgrade. When the user isn't entitled, send them to hosted checkout with
purchaseUrl(priceId)— UseThatApp is the merchant of record (payment, tax, refunds), and the buyer returns signed in with the entitlement already live, no webhooks.getPrices()renders your live pricing page;manageUrl()links entitled users to subscription management.
Installation
npm install usethatappThese docs describe 2.2.0 or newer; if you are upgrading from an earlier release, run npm install usethatapp@latest first — the License Key API, product_id, and UtaServiceNotEnabledError do not exist before it.
Prerequisites
- Node.js 18+, server-side only. The SDK uses the global
fetchplus Node'snode:cryptoandnode:fs— it does not run on Edge runtimes (Cloudflare Workers, Vercel Edge, Next.jsmiddleware.ts) or in a browser bundle. Do entitlement checks in a Node server route. - ESM-only. Set
"type": "module"inpackage.jsonor use the.mjsextension.require("usethatapp")from CommonJS is not supported — from a CJS file, use dynamic import:const uta = await import("usethatapp"); - An OAuth client registered on your app's Integration page — see OpenID Connect for registering your redirect URI and copying credentials.
Configuration
The SDK reads from process.env by default; override any setting programmatically with configure({...}). Only the client id, secret, and redirect URI are required; the rest default to production. Keep the secret server-side.
UTA_CLIENT_ID=... # required
UTA_CLIENT_SECRET=... # required (server-side only)
UTA_REDIRECT_URI=https://yourapp.com/callback
# optional — default to production:
UTA_ISSUER=https://www.usethatapp.com/o
UTA_API_URL=https://www.usethatapp.com
UTA_SCOPES="openid entitlements"If your hosting provider mounts secrets as files, set UTA_CLIENT_SECRET_PATH instead of UTA_CLIENT_SECRET and the SDK reads the secret from that file.
Public API
Everything imports from "usethatapp". All calls are async and return Promises.
import {
beginLogin, // () => { authorizationUrl, flowState }
completeLogin, // ({ code, state, flowState }) => UtaSession
getEntitlement, // (accessToken) => Entitlement
refresh, // (refreshToken) => UtaSession
userinfo, // (accessToken) => { sub }
logoutUrl, // ({ idToken, postLogoutRedirectUri }) => string
// Selling — hosted checkout & live pricing
purchaseUrl, // (priceId, { next, ref, email }?) => string — pure URL builder
manageUrl, // ({ next }?) => string — subscription management, pure
getPrices, // () => Promise<AppPrices> — live prices, no auth
getAppInfo, // () => Promise<AppInfo> — public app profile, no auth
configure, // override env config
resetConfig,
// Errors (all extend UtaError)
UtaError,
UtaConfigError,
UtaDiscoveryError,
UtaAuthError,
UtaTokenError, // 401
UtaPermissionError, // 403
UtaServerError,
// Types (TypeScript)
type UtaSession,
type Entitlement,
type UtaFlowState,
type AppInfo,
type AppPrices,
type UtaPrice,
} from "usethatapp";UtaSession and Entitlement
completeLogin() returns a UtaSession — the user's pairwise pseudonymous sub plus tokens. Key your user records off sub, never an email (none is shared). getEntitlement() returns an Entitlement — gate on the stable product_public_id, display the mutable version.
interface UtaSession {
readonly sub: string; // pairwise, stable per-app — your local user key
readonly access_token: string; // pass to getEntitlement
readonly expires_at: number; // unix seconds
readonly refresh_token: string | null; // null when no refresh token was granted
readonly id_token: string | null; // pass to logoutUrl
readonly scope: string;
readonly token_type: string;
readonly claims: Record<string, unknown>; // validated ID-token claims
}
interface Entitlement {
readonly entitled: boolean; // may the user use the app
readonly version: string | null; // plan display name, e.g. "Pro" — mutable
readonly product_id: string | null; // stable prod_... id — gate features on this
readonly product_public_id: string | null; // same prod_... value — equal-valued alias
readonly status: string; // active / trialing / free / preview / none / ...
readonly is_free: boolean;
readonly period_end: string | null;
readonly raw: Record<string, unknown>; // the full decoded response
}AppPrices, UtaPrice, and AppInfo
The selling surface. getPrices() returns your app's live prices — never hardcode them: each price carries a ready-made buy_url and the product_id that matches the entitlement’s product_public_id you gate on. Both GET helpers are anonymous (no login); a 404 (app unknown, unpublished, or external sales not enabled) raises UtaError, and a 429 rate limit raises a retriable UtaServerError. See the Pricing API and Sell from your own website.
interface AppPrices {
readonly client_id: string;
readonly app_name: string;
readonly has_free_tier: boolean;
readonly prices: readonly UtaPrice[]; // readonly — copy with [...prices] before sorting
readonly raw: Record<string, unknown>;
}
interface UtaPrice {
readonly public_id: string; // prc_... — pass to purchaseUrl()
readonly product_id: string; // matches the entitlement's product_public_id
readonly product_name: string;
readonly amount: string; // decimal string, e.g. "10.00"
readonly currency: string; // lowercase ISO code, e.g. "usd"
readonly is_recurring: boolean;
readonly frequency: string | null; // "day" | "week" | "month" | "year"; null = one-time
readonly buy_url: string; // ready-made hosted-checkout buy link
}
interface AppInfo {
readonly client_id: string;
readonly name: string;
readonly tagline: string;
readonly listing_mode: string; // "marketplace" or "external"
readonly url: string;
readonly marketplace_url: string;
readonly raw: Record<string, unknown>;
}Quickstart — any framework
import { beginLogin, completeLogin, getEntitlement } from "usethatapp";
// 1) Start login — however your framework spells "redirect":
const { authorizationUrl, flowState } = await beginLogin();
saveToSession("utaFlow", flowState); // JSON-serializable
res.redirect(authorizationUrl);
// 2) In your callback (reads ?code=...&state=... off the request).
// On cancel/deny the provider sends ?error=... and no code — handle it first:
if (req.query.error) return res.redirect("/"); // login was canceled
const session = await completeLogin({
code: req.query.code,
state: req.query.state,
flowState: loadFromSession("utaFlow"),
});
saveToSession("utaSub", session.sub);
saveToSession("utaAccessToken", session.access_token);
// 3) Anywhere you gate features:
const ent = await getEntitlement(loadFromSession("utaAccessToken"));
if (ent.entitled && ent.product_public_id === "prod_<your-pro-id>") { /* ... */ }⚠ Browser apps need a backend.
The SDK is a confidential client — it must never ship to the browser. A single-page app (React, Vue) runs the SDK in a small backend (a "backend-for-frontend") and calls that from the browser. See the React guide for the pattern.
Choose your framework
Each guide is a complete walkthrough with the tested example from the SDK.
Express
/login, /callback, /logout routes with express-session, plus entitlement gating.
Node.js (no framework)
Plain node:http server with a tiny cookie session. The same routes, no dependencies.
Fastify
The same confidential flow in Fastify idioms — @fastify/session and reply.redirect.
Next.js
App Router route handlers for login and callback, with the SDK on the server only.
Nuxt
Nuxt 3 / h3 server handlers for the OIDC flow, server-side only.
React
Pair a React UI with a backend-for-frontend that runs the confidential SDK.
Vue
Pair a Vue UI with a Nuxt backend-for-frontend that runs the confidential SDK.
Next steps
Review Error Handling for the typed errors getEntitlement raises, and Gotchas for the edge cases.
