Skip to content
BeeuniBeeuni
English
Esc
↑↓navigate↵open⌘Jpreview
On this page

Sign in with Beeuni

Gate your own app behind Beeuni identity with one route, and act as the signed-in user through the SDK.

“Sign in with Beeuni” makes Beeuni the identity provider for an app you run: a dashboard, an internal tool, a vertical product built on Beeuni. Your users sign in with their Beeuni account, your server knows who they are, and every Beeuni call your app makes runs as that user with that user’s role assignments. The whole flow lives in @beeuni/sdk. Your app never stores a Beeuni token in the browser.

It is standard OAuth 2.1 (authorization code + PKCE) served by the Beeuni API at api.beeuni.ai.

1. Register your app

Go to Account → Tokens → OAuth apps → Register app, or call the SDK:

const app = await beeuni.iam.oauthClients.create(accountId, {
  name: 'Dashboards',
  client_type: 'confidential',            // 'public' for a browser/native app (PKCE only, no secret)
  redirect_uris: ['https://dashboards.example.com/api/beeuni/auth/callback'],
  scopes: ['profile', 'email', 'beeuni'],
});
// app.client_id, app.client_secret (shown once)

Registration needs token.create on the account. Redirect URIs are compared byte for byte; https is required except on localhost.

Scope Grants the app
profile The user’s id, email and account memberships (GET /v1/accounts/me).
email The email address (an alias for OIDC-shaped clients).
beeuni Acting as the user on the whole Beeuni API — projects, sessions, files, IAM probes. Without it the token is identity-only.

2. Mount the handler

// lib/beeuni-auth.ts
import { createBeeuniAuth } from '@beeuni/sdk/server';

export const auth = createBeeuniAuth({
  backendUrl: 'https://api.beeuni.ai/v1',
  clientId: process.env.BEEUNI_OAUTH_CLIENT_ID!,
  clientSecret: process.env.BEEUNI_OAUTH_CLIENT_SECRET,   // omit for a public client
  redirectUri: 'https://dashboards.example.com/api/beeuni/auth/callback',
  cookieSecret: process.env.BEEUNI_AUTH_COOKIE_SECRET!,   // ≥ 32 chars; encrypts the session cookie
});
// app/api/beeuni/auth/[...beeuni]/route.ts  (Next.js App Router)
import { auth } from '@/lib/beeuni-auth';
const handle = (request: Request) => auth.handler(request);
export { handle as GET, handle as POST };

The handler serves every route under basePath (derived from the redirect URI — /api/beeuni/auth above):

Path Does
/signin?return_to=/path Starts sign-in (PKCE S256 + state in a 10-minute cookie) and redirects to Beeuni.
/callback Exchanges the code, sets the encrypted HttpOnly session cookie, redirects to return_to.
/refresh?return_to= Rotates the token pair and redirects. Used by requireViewer.
/signout?return_to= Revokes the refresh token at Beeuni and clears the cookie.
/me The viewer as JSON, or 401. Refreshes inline when the access token expired.
/proxy/* Forwards to the Beeuni API as the viewer. The browser SDK’s backendUrl.

return_to is always confined to a same-origin path.

3. Gate pages and act as the user

// middleware.ts — every page needs a viewer
import { auth } from '@/lib/beeuni-auth';

export async function middleware(request: Request) {
  const gate = await auth.requireViewer(request);
  if (gate.response) return gate.response;   // 302 → /refresh or /signin
}
export const config = { matcher: ['/((?!api/beeuni/auth|_next).*)'] };
// a server component / route handler
const viewer = await auth.viewer(request);       // { userId, email, accounts, scopes, token, expiresAt } | null
const beeuni = await auth.beeuni(request);       // request-scoped client acting as the viewer
const projects = await beeuni.projects.list();
const allowed = await beeuni.iam.can(accountId, viewer!.userId, { action: 'project.write', resourceType: 'project', resourceId });

viewer() is read-only and never consumes the single-use refresh token; use requireViewer() in middleware so a page never renders signed-out for a user whose refresh token is still good.

4. The browser

import { createBeeuni } from '@beeuni/sdk';
import { SignInWithBeeuni, useBeeuniViewer } from '@beeuni/sdk/react';

const beeuni = createBeeuni(auth.clientConfig());   // backendUrl = '/api/beeuni/auth/proxy'

function Header() {
  const { status, viewer } = useBeeuniViewer();
  if (status === 'signed-in') return <span>{viewer.email}</span>;
  return <SignInWithBeeuni className="button" />;
}

The browser client sends a sentinel bearer; /proxy swaps it for the viewer’s real token on the server. useSession, beeuni.project(id).sessions.* and every other SDK call work unchanged through it.

What the user sees

The first time, Beeuni shows a consent screen naming your app and the scopes. Beeuni remembers the decision per user and app, so later sign-ins redirect straight back. Revoking an app deletes every token it minted.

Discovery

GET https://api.beeuni.ai/.well-known/oauth-authorization-server (also under /v1/oauth/.well-known/…) publishes the endpoints for a generic OAuth client. The SDK does not need it — it derives every endpoint from backendUrl.

Sign-in tokens for Apps

Code in an App hosted by Beeuni, and the Apps it uses, accepts a second kind of token: a short ES256 JWT that Beeuni mints for one App. No OAuth flow, no redirect, no client secret.

Value
Issuer <API origin>/v1/projects/<projectId>, one per project
Discovery <issuer>/.well-known/openid-configuration and <issuer>/jwks.json, public
Audience The id of the App the token is for
Lifetime 15 minutes
Claims sub, email, name, picture, groups, group_ids, role, account_id, project_id; an agent’s token adds kind: "agent" and has no groups or role

Every App carries the values that verify its tokens: app.auth.issuer, app.auth.audience, app.auth.jwks_uri.

import { beeuniToken, beeuniBinding, verifyBeeuniToken, requireBeeuniMember } from '@beeuni/sdk';

// In the App's browser code: a token fetcher for this App, or an App it uses.
const forThisApp = beeuniToken();
const forDb = beeuniToken({ audience: 'db' });
const db = beeuniBinding('db');   // { url: '<origin>/_beeuni/apps/db', token: forDb }

// On a server that receives the token:
const member = await verifyBeeuniToken(bearer, {
  issuer: app.auth.issuer,
  audience: app.auth.audience,
  jwks: app.auth.jwks_uri,
});
requireBeeuniMember(member, { roles: ['owner', 'admin'] });

In a server App, Beeuni injects BEEUNI_AUTH_ISSUER (the project issuer), BEEUNI_AUTH_AUDIENCE (the App’s own id) and BEEUNI_AUTH_JWKS (the project key set, as a data: URI), so verifyBeeuniToken(bearer) needs no options. Do not set BEEUNI_AUTH_* yourself: the BEEUNI_ prefix is reserved and beeuni validate rejects it.

verifyBeeuniToken refuses a token when no audience is set (the option or BEEUNI_AUTH_AUDIENCE): one project key signs the tokens of every App in the project, so aud is what keeps another App’s token out. Pass audience: false only to accept a token for any App of the project.

beeuniToken reads GET /_beeuni/token?audience=<slug or id> on the App’s own origin. The audience is the App itself (the default) or an App it uses (beeuni apps link <app> --uses <other>); any other answers 403 app_not_linked. A member, a script or an agent mints one through the API instead: beeuni.project(projectId).apps.token(appId), or beeuni apps token <app>. The full contract, the bindings mount and the Convex backend setup: SDK → Apps and Apps → Apps an App uses.

Was this page helpful?