Base IdP

Base IdP

Getting Started

Environment

The minimal Base IdP env surface by role. One value per app, sometimes none.

Base IdP keeps env tiny on purpose. The whole configuration philosophy can be written on one line: the human types at most one value per app. Everything else — issuer, audience, scopes, keys — is resolved from the registration at runtime by the SDK.

If a setup doc somewhere asks you to paste an issuer URL into a .env file, that document is out of date. Stop reading it.

This page is the contract. Every SDK, every framework guide, and the CLI all implement what is on this page.

The three roles

Pick the role your code plays. The role decides which env you need.

What it is

A frontend that runs the login in a system browser using PKCE. The user authenticates, the browser hands you back an authorization code, and the client SDK exchanges that code for tokens without a secret.

Examples: a Flutter app, a React Native or Expo app, a SwiftUI iOS app, a browser-only Next.js app that talks to a separate API.

The env block

.env
BASE_IDP_CLIENT_ID=sq_live_yourapp

That is the entire file. Nothing else is required. On Expo this becomes EXPO_PUBLIC_BASE_IDP_CLIENT_ID. On a browser-only Next.js it becomes NEXT_PUBLIC_BASE_IDP_CLIENT_ID. The name follows your framework's convention for "values that are safe to ship in the bundle."

Why no secret

A secret in a public bundle is a leaked secret. The client id, on the other hand, is not a secret — it is the public identifier of your app. Shipping it is fine; users see it in the address bar of the login page.

What the SDK does

  • Builds the authorize URL with PKCE.
  • Opens the system browser.
  • Receives the redirect and exchanges the code with a code_verifier.
  • Caches the access token and refresh token in secure storage.

What it is

An API that receives a token (in an Authorization: Bearer header, or from a mobile SDK's exchange payload) and only needs to know who the user is. It does not run the login. It does not call the token endpoint. It only verifies.

Examples: a Go service behind a mobile app, a Rust gateway, a Node API behind a React Native app, an internal microservice that trusts upstream tokens.

The env block

.env
# Verify-only — nothing here is strictly required. The token carries the
# audience, the public keys are published, and the issuer is the SDK default.
# Optionally pin the expected audience by setting your client id:
# BASE_IDP_CLIENT_ID=sq_live_yourapp

You can ship a verify-only service with no Base IdP env at all. The only reason to set the client id is to pin the audience check, so a token minted for a different app is rejected.

Why no secret

A verify-only service never calls the token endpoint. There is no situation in which it authenticates as the app. It only checks PASETO signatures and reads claims. No secret is involved.

What the SDK does

  • Fetches the public keys at startup and caches them.
  • Verifies the Ed25519 signature on each incoming token.
  • Re-fetches the key set if a token references a kid it has not seen, so key rotation is automatic.
  • Returns a typed principal: subject, email, name, scopes, role.

What it is

A server that runs the login itself — the code-to-token exchange happens on your server, not on a user's device. The server authenticates to Base IdP with a secret on the token endpoint.

Examples: a Next.js app where the same server renders pages and handles auth callbacks, a NestJS web app, a Laravel app, a server-rendered Rails app.

The env block

.env
BASE_IDP_CLIENT_ID=sq_live_yourapp
BASE_IDP_CLIENT_SECRET=sqk_3b7157c3d69f11a4af17d6c955ae31fd1fe3183e10b329c1ef4220c3badc0c18

Keep this file out of version control. Use your platform's secrets manager (fly secrets, vercel env, Kubernetes secrets, GitHub Actions secrets) for deployment.

Why a secret

The token endpoint requires client authentication for confidential clients. The secret proves the request is really coming from your registered server, not from a forged client.

What the SDK does

  • Builds the authorize URL.
  • Receives the redirect on your callback route.
  • Calls POST /oauth2/token with the secret in the body (or in a Basic auth header, depending on configuration).
  • Verifies the returned access token.
  • Hands you back a tokens object and a principal.

Fullstack apps are server-only

A Next.js app where the same server runs the login is not split between browser and server. It is entirely server-side. The browser navigates to /api/auth/start, the server builds the authorize URL and redirects to Base IdP, Base IdP redirects back to /api/auth/callback, the server exchanges the code for tokens and stores them as httpOnly cookies. At no point does the browser know the client id, the secret, or the issuer.

The env block is the confidential server block, exactly as written above. Neither variable gets a NEXT_PUBLIC_ prefix. The browser never sees them.

app/api/auth/start/route.ts
import "server-only";
import { NextRequest, NextResponse } from "next/server";

// process.env.BASE_IDP_CLIENT_ID — no NEXT_PUBLIC_ prefix.
// This file never ships to the browser.
export async function GET(request: NextRequest) {
  const clientId = process.env.BASE_IDP_CLIENT_ID!;
  const codeVerifier = generateCodeVerifier();
  const authorizeUrl = buildAuthorizeUrl(clientId, codeVerifier, request);
  // Store codeVerifier server-side, redirect to Base IdP.
  return NextResponse.redirect(authorizeUrl);
}
app/api/auth/callback/route.ts
import "server-only";
import { NextRequest, NextResponse } from "next/server";

// Secret lives here, server-side only.
export async function GET(request: NextRequest) {
  const clientId = process.env.BASE_IDP_CLIENT_ID!;
  const clientSecret = process.env.BASE_IDP_CLIENT_SECRET!;
  const code = request.nextUrl.searchParams.get("code")!;
  const tokens = await exchangeCode(code, clientId, clientSecret);
  // Store tokens as httpOnly cookies, redirect to app.
  const response = NextResponse.redirect(new URL("/dashboard", request.url));
  response.cookies.set("session", tokens.access_token, { httpOnly: true });
  return response;
}

The browser component that signs the user in is a plain anchor tag — no SDK, no client id, no Base IdP knowledge at all.

app/sign-in/page.tsx
export default function SignInPage() {
  return <a href="/api/auth/start">Sign in</a>;
}

This is the pattern used by Square's own console app. All Base IdP env vars are server-only. Nothing leaks to the browser bundle.

What is no longer in env

These two values used to appear in some examples. They are not part of the contract anymore.

BASE_IDP_ISSUER

The issuer is https://authlayer.squareexp.com. The SDK uses it by default. You only override it for local development against a local Base IdP instance.

.env.local (development only)
BASE_IDP_ISSUER=http://localhost:8080

If your local-dev script sets this and your production env does not, that is the correct shape. In production you should never see this line.

BASE_IDP_AUDIENCE

The audience is carried inside the token. The SDK reads it. The client config endpoint also exposes the audience for an app, so the SDK pins it correctly. You do not set this anywhere.

Why frontend and backend use different SDKs

This is a question that comes up a lot, so it is worth answering plainly. Frontend and backend use different SDKs because they do different jobs.

The frontend SDK does login: PKCE, opening the browser, handling the redirect, holding tokens in secure storage. It is a public client and never holds a secret.

The backend SDK does verification: parsing PASETO tokens, checking signatures against published keys, returning a principal. Optionally it also runs the server-side login, in which case it holds a secret.

These are two different jobs with two different threat models, so they are two different libraries. This is the same way every major identity provider works.

ProviderFrontendBackend
GoogleGoogle Identity Services (JS)google-auth-library per language
FirebaseClient SDKAdmin SDK
Auth0auth0-spa-jsper-language verifier
Base IdPclient SDKverifier SDK

You only end up with two languages if your frontend and backend are two languages. That is your architecture choice, not Base IdP's requirement. A Next.js fullstack app is one language on both sides.

Where to go next

On this page