Public Quickstart
Purpose: Start PKCE and user-session flows from frontend or mobile code.
Use this when: You selected Frontend or mobile app in Developer Console and are writing browser or mobile code.
Do not use this when: You need Content, Search, or client_secret.
Backend required: No for OAuth; browser-origin restrictions may still require an API proxy.
Allowed runtimes: Browser apps, mobile apps.
Required credentials: client_id.
Minimal import: @quranjs/api/public.
Minimal Example
This browser example generates an RFC 7636 S256 PKCE pair. React Native apps should use a secure platform crypto library or an OAuth library such as Expo AuthSession with PKCE enabled.
import { createPublicClient } from "@quranjs/api/public";
const PKCE_STORAGE_KEY = "qf:oauth:pending";
const redirectUri = "http://localhost:3000/callback";
const base64url = (bytes: Uint8Array) =>
btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
async function createPkcePair() {
const verifierBytes = crypto.getRandomValues(new Uint8Array(32));
const codeVerifier = base64url(verifierBytes);
const challengeBytes = new Uint8Array(
await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(codeVerifier),
),
);
return { codeVerifier, codeChallenge: base64url(challengeBytes) };
}
const client = createPublicClient({
clientId: "your-client-id",
clientType: "public",
services: {
gatewayUrl: "https://apis-prelive.quran.foundation",
oauth2BaseUrl: "https://prelive-oauth2.quran.foundation",
},
});
const state = crypto.randomUUID();
const nonce = crypto.randomUUID();
const { codeVerifier, codeChallenge } = await createPkcePair();
sessionStorage.setItem(
PKCE_STORAGE_KEY,
JSON.stringify({ codeVerifier, nonce, state }),
);
const authUrl = client.oauth2.v1.authorizeUrl({
client_id: "your-client-id",
redirect_uri: redirectUri,
response_type: "code",
scope: "openid offline_access user bookmark collection",
state,
nonce,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
window.location.assign(authUrl);
The service URLs explicitly keep a newly created Console app in pre-live. After production permissions are granted, switch the Client ID and both service URLs to production together.
Exchange the Callback Code
On the registered callback page, validate state, consume the stored verifier once, and pass it to the public SDK exchange:
const callback = new URLSearchParams(window.location.search);
const code = callback.get("code");
const returnedState = callback.get("state");
const pendingJson = sessionStorage.getItem(PKCE_STORAGE_KEY);
const pending = pendingJson
? (JSON.parse(pendingJson) as {
codeVerifier: string;
nonce: string;
state: string;
})
: null;
if (!code || !pending || returnedState !== pending.state) {
throw new Error("Invalid OAuth callback state");
}
sessionStorage.removeItem(PKCE_STORAGE_KEY);
await client.oauth2.v1.exchangeCode({
code,
codeVerifier: pending.codeVerifier,
redirectUri,
});
Validate the ID token and its nonce with an OIDC-capable library before trusting identity claims. See the manual OAuth2 tutorial for the complete validation and refresh requirements.
If You Already Have a User Session
const client = createPublicClient({
clientId: "your-client-id",
clientType: "public",
services: {
gatewayUrl: "https://apis-prelive.quran.foundation",
oauth2BaseUrl: "https://prelive-oauth2.quran.foundation",
},
userSession: {
accessToken: "user-access-token",
},
});
const collections = await client.auth.v1.collections.list();
With an existing user session and the post scope, the public client can use
the same QuranReflect post helper shape documented in
QuranReflect Posts.
Common Mistakes
- Putting
client_secretin frontend code. - Calling Content or Search from
public. - Calling
oauth2/tokendirectly for a confidential client. - Using
clientType: "confidential-proxy"when the Console app type is Frontend or mobile app.