SDK

Sign in with SingulaComp

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

GithubEdit

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

It is standard OAuth 2.1 (authorization code + PKCE) served by the SingulaComp API, so it works the same against api.singulacomp.ai and against a self-hosted instance.

Building an App hosted by SingulaComp (*.apps.singulacomp.ai)? You need none of this. The Apps gate already authenticated the viewer — read them with singulacompAppViewerToken() / readAppViewer(). See Apps → Your App already knows who is looking.

1. Register your app

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

typescript
const app = await singulacomp.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/singulacomp/auth/callback'],
  scopes: ['profile', 'email', 'singulacomp'],
});
// 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.

ScopeGrants the app
profileThe user's id, email and account memberships (GET /v1/accounts/me).
emailThe email address (an alias for OIDC-shaped clients).
singulacompActing as the user on the whole SingulaComp API — projects, sessions, files, IAM probes. Without it the token is identity-only.

2. Mount the handler

typescript
// lib/singulacomp-auth.ts
import { createSingulaCompAuth } from '@singulacomp/sdk/server';

export const auth = createSingulaCompAuth({
  backendUrl: 'https://api.singulacomp.ai/v1',
  clientId: process.env.SINGULACOMP_OAUTH_CLIENT_ID!,
  clientSecret: process.env.SINGULACOMP_OAUTH_CLIENT_SECRET,   // omit for a public client
  redirectUri: 'https://dashboards.example.com/api/singulacomp/auth/callback',
  cookieSecret: process.env.SINGULACOMP_AUTH_COOKIE_SECRET!,   // ≥ 32 chars; encrypts the session cookie
});
typescript
// app/api/singulacomp/auth/[...singulacomp]/route.ts  (Next.js App Router)
import { auth } from '@/lib/singulacomp-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/singulacomp/auth above):

PathDoes
/signin?return_to=/pathStarts sign-in (PKCE S256 + state in a 10-minute cookie) and redirects to SingulaComp.
/callbackExchanges 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 SingulaComp and clears the cookie.
/meThe viewer as JSON, or 401. Refreshes inline when the access token expired.
/proxy/*Forwards to the SingulaComp 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

typescript
// middleware.ts — every page needs a viewer
import { auth } from '@/lib/singulacomp-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/singulacomp/auth|_next).*)'] };
typescript
// a server component / route handler
const viewer = await auth.viewer(request);       // { userId, email, accounts, scopes, token, expiresAt } | null
const singulacomp = await auth.singulacomp(request);       // request-scoped client acting as the viewer
const projects = await singulacomp.projects.list();
const allowed = await singulacomp.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

tsx
import { createSingulaComp } from '@singulacomp/sdk';
import { SignInWithSingulaComp, useSingulaCompViewer } from '@singulacomp/sdk/react';

const singulacomp = createSingulaComp(auth.clientConfig());   // backendUrl = '/api/singulacomp/auth/proxy'

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

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

What the user sees

The first time, SingulaComp shows a consent screen naming your app and the scopes. SingulaComp 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.singulacomp.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.

On this page