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
- 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.
- Put the client ID (your app id) and secret in your app's server environment as
INVERSE_CLIENT_IDandINVERSE_CLIENT_SECRET. - Use
/auth/callbackon 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
- Send the person to
/oauth/authorize?client_id&redirect_uri&state&scope=profile entitlement. - They sign in to Inverse if needed and allow your app once. Inverse remembers the answer.
- Inverse sends them to
redirect_uri?code&state. Checkstateagainst the one you stored. The code works once, for 60 seconds. - Your server exchanges the code for an access token, the user and their entitlement.
npm install inverse-embedimport { 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 }));
}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
| Request | Returns |
|---|---|
GET /oauth/authorize | client_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/me | With Authorization: Bearer <access_token>: { user, entitlement }. |
| Field | Meaning |
|---|---|
user.id | A per-app id. The same person gets a different id in every app. |
user.name | Their first name, only if they chose to share it. |
entitlement.active | Whether 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.kind | once, sub, owner, free or null. |
entitlement.status / until | For a subscription: active, past_due or canceled, and the paid-through time in ms. |