inversedocs
Pages

Sign in with Inverse

Charge once or by subscription, then let people sign in to your app on its own domain with their Inverse account, so your server knows who they are and whether they've paid.

Pricing

In /dev, pick Free, Once, Monthly or Yearly and a price. People pay with Stripe Checkout. A one-time purchase opens the app for good; a subscription opens it while it's paid, and for a past-due one until the end of the paid period. Your Inverse room link checks this for you.

Set up

  1. In /dev, open your app and press Create client secret under Sign in with Inverse. Copy it: it's shown once. Rotate it any time; the old one stops working.
  2. Put the client ID (your app id) and secret in your app's server environment as INVERSE_CLIENT_ID and INVERSE_CLIENT_SECRET.
  3. Use /auth/callback on your hosted URL or a verified custom domain; it's allowed automatically. Add other paths on those hosts under Extra redirect URIs. Redirect URIs must match exactly and use https.

The flow

  1. Send the person to /oauth/authorize?client_id&redirect_uri&state&scope=profile entitlement.
  2. They sign in to Inverse if needed and allow your app once. Inverse remembers the answer.
  3. Inverse sends them to redirect_uri?code&state. Check state against the one you stored. The code works once, for 60 seconds.
  4. Your server exchanges the code for an access token, the user and their entitlement.
terminal
npm install inverse-embed
app/auth/login/route.ts
import { cookies } from "next/headers";
import { authorizeUrl, newState } from "inverse-embed/auth";

export async function GET(request: Request) {
  const state = newState(), origin = new URL(request.url).origin;
  (await cookies()).set("inverse_state", state, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 600 });
  return Response.redirect(authorizeUrl({ redirectUri: `${origin}/auth/callback`, state }));
}
app/auth/callback/route.ts
import { cookies } from "next/headers";
import { exchangeCode } from "inverse-embed/auth";

export async function GET(request: Request) {
  const url = new URL(request.url), jar = await cookies();
  if (!url.searchParams.get("state") || url.searchParams.get("state") !== jar.get("inverse_state")?.value) return new Response("Sign-in expired. Try again.", { status: 400 });
  const { access_token, user, entitlement } = await exchangeCode({
    code: url.searchParams.get("code"),
    redirectUri: `${url.origin}/auth/callback`, // clientId/secret from INVERSE_CLIENT_ID / INVERSE_CLIENT_SECRET
  });
  jar.set("inverse_token", access_token, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 3600 });
  jar.delete("inverse_state");
  console.log("signed in", user.id); // a per-app id; never an email
  return Response.redirect(new URL(entitlement.active ? "/" : "/subscribe", url));
}

Later, getMe(token) returns the same user and the entitlement as of now. Or let the package do all three routes: inverseAuth() returns login, callback and session handlers for Next.js route handlers, and expressAuth() the same for Express.

API

RequestReturns
GET /oauth/authorizeclient_id, redirect_uri, state, scope (profile entitlement). Redirects to redirect_uri?code&state, or ?error=access_denied&state.
POST /api/oauth/token{ grant_type: "authorization_code", code, client_id, client_secret, redirect_uri } as JSON or a form. Returns { access_token, expires_in: 3600, user: { id, name? }, entitlement }.
GET /api/oauth/meWith Authorization: Bearer <access_token>: { user, entitlement }.
FieldMeaning
user.idA per-app id. The same person gets a different id in every app.
user.nameTheir first name, only if they chose to share it.
entitlement.activeWhether they may use the app now: you, a free app, a one-time purchase, or a subscription that's active (or past_due before until).
entitlement.kindonce, sub, owner, free or null.
entitlement.status / untilFor a subscription: active, past_due or canceled, and the paid-through time in ms.