SkyCanvasDocs
Selected integration guide

Add SkyCanvas SSO to your application

Pick the app you are integrating. The page will show only the setup that applies to it.

What kind of app are you adding SSO to?

Choose the closest match. You can switch without losing anything.

Selected guide

Full-stack standalone

Start this guide
Typical setup10–15 minutes
Configuration3 server values
You getFirst-party HttpOnly session
01

Install

Add the SSO package

02

Connect server

Configure and mount auth

03

Use session

Sign in, protect, sign out

React applications on this standalone path must mount one SsoProvider. Pass it the bootstrap returned by the server helper so it receives the initial session and creates its browser client without another configuration file.

Architecture

Choose who owns the application session

SkyCanvas always owns central identity and upstream OAuth callbacks. Your integration choice decides how the consuming application proves authentication on later requests.

IntegrationBest forSession ownerApp credentialNew auth server?
React-onlyVite/SPA frontend with a separate API or no backendSkyCanvas SDK in the browserShort-lived app access tokenNo
Full-stack standaloneTanStack Start, Next.js, Elysia, Express, or NestJS@skycanvasstudio/sso server adapterEncrypted first-party HttpOnly cookieSmall framework adapter
Better AuthThe app already has working Better AuthBetter AuthBetter Auth first-party cookieExisting Better Auth route
Generic OAuth/OIDCAnother auth library already owns app sessionsYour existing auth libraryLibrary-owned cookie/sessionExisting auth callback

Selected guide

Use SSO in a full-stack TypeScript app

Complete these steps in order. OAuth tokens and session secrets stay on the server for this integration.

  1. 1

    Install one package

    No Better Auth package, auth database, or consumer-side OAuth plugin is required.

    Terminal
    bun add @skycanvasstudio/sso
  2. 2

    Server environment (required)

    Put these values in your server-only environment module. APP_URL is the public origin of this app and prevents a container, proxy, or server bound to 0.0.0.0 from generating an invalid callback. SKYCANVAS_SECRET_KEY must never reach browser code.

    .env — server only
    # Server only — do not prefix these with VITE_ or NEXT_PUBLIC_
    SKYCANVAS_PUBLISHABLE_KEY=your_client_id
    SKYCANVAS_SECRET_KEY=replace_with_at_least_32_random_characters
    SKYCANVAS_SSO_URL=https://api-sso.skycanvasstudio.com
    APP_URL=http://localhost:3000
  3. 3

    Client environment (not needed)

    The packaged UI gets its safe configuration from the server bootstrap and local auth routes. Do not create VITE_SKYCANVAS_* or NEXT_PUBLIC_SKYCANVAS_* variables—not even for the publishable key.

    No client .env entry
    No browser environment variables are required.
  4. 4

    Configure SkyCanvas once

    Choose embedded forms or the hosted page here. Popup is recommended for social OAuth; redirect remains available. The SDK provides an immediate popup loading screen while it prepares and opens SkyCanvas, so no application popup route is needed.

    src/lib/skycanvas.server.ts
    import { createTanStackSso } from "@skycanvasstudio/sso/tanstack-start"
    import { env } from "./env.server"
    
    export const skycanvas = createTanStackSso({
      publishableKey: env.SKYCANVAS_PUBLISHABLE_KEY,
      secretKey: env.SKYCANVAS_SECRET_KEY,
      ssoUrl: env.SKYCANVAS_SSO_URL,
      appUrl: env.APP_URL,
      interactionMode: "embedded", // "hosted" redirects to the SSO auth page
      oauthMode: "popup",          // or "redirect"
    })
  5. 5

    Add one middleware

    It mounts the package auth routes and makes the verified session available as context.skycanvasAuth.

    src/start.ts
    import { createServerOnlyFn, createStart } from "@tanstack/react-start"
    import { createTanStackSsoMiddleware } from "@skycanvasstudio/sso/tanstack-start"
    
    const loadSkycanvas = createServerOnlyFn(
      () => import("./lib/skycanvas.server").then(({ skycanvas }) => skycanvas),
    )
    const skycanvasMiddleware = createTanStackSsoMiddleware(
      loadSkycanvas,
    )
    
    export const startInstance = createStart(() => ({
      requestMiddleware: [skycanvasMiddleware],
    }))
  6. 6

    Optional: receive user webhooks

    This is separate from app/auth/[...sso]/route.ts. Configure the webhook once for the SkyCanvas application through the SSO admin API: PUT /admin/applications/:applicationId/webhooks. This is application-level delivery configuration, not an OAuth client setting and not an option passed to createNextSso(). Save the one-time returned secret as SSO_WEBHOOK_SECRET in this app's server environment.

    app/api/sso/webhooks/route.ts
    import { createWebhookHandler } from "@skycanvasstudio/sso/server"
    import { env } from "@/env"
    import { prisma } from "@/lib/prisma"
    
    export const POST = createWebhookHandler(
      {
        "user.created": async ({ data }) => {
          await prisma.user.upsert({
            where: { ssoUserId: data.id },
            create: { ssoUserId: data.id, name: data.name, email: data.email, image: data.image },
            update: { name: data.name, email: data.email, image: data.image },
          })
        },
        "user.updated": async ({ data }) => {
          await prisma.user.upsert({
            where: { ssoUserId: data.id },
            create: { ssoUserId: data.id, name: data.name, email: data.email, image: data.image },
            update: { name: data.name, email: data.email, image: data.image },
          })
        },
        "user.deleted": async ({ data }) => {
          await prisma.user.deleteMany({ where: { ssoUserId: data.id } })
        },
      },
      { secret: env.SSO_WEBHOOK_SECRET },
    )
  7. 7

    Repair a missed webhook on sign-in

    Webhooks keep an app synchronized while the user is away. Add onSignIn as a recovery path: it runs after the SSO identity is verified and before the local application session is created. Always use a unique ssoUserId column; do not assume the local primary key is the SSO user ID.

    src/lib/skycanvas.ts
    export const skycanvas = createNextSso({
      // publishableKey, secretKey, ssoUrl, and appUrl from the earlier step
      onSignIn: async ({ user }) => {
        const localUser = await prisma.user.upsert({
          where: { ssoUserId: user.id },
          create: { ssoUserId: user.id, name: user.name, email: user.email, image: user.image },
          update: { name: user.name, email: user.email, image: user.image },
        })
        return { ...user, localUserId: localUser.id }
      },
    })
  8. 8

    Use the packaged UI anywhere

    SignIn, SignUp, SsoAuth, and SsoAuthDialog automatically show only enabled methods. After successful password or popup OAuth authentication, returnTo safely navigates the original window. Provide onSuccess when your app should control navigation itself.

    src/routes/login.tsx
    import "@skycanvasstudio/sso/styles.css"
    import { SignIn, SkyCanvasProvider } from "@skycanvasstudio/sso/react"
    
    export function LoginPage() {
      return (
        <SkyCanvasProvider>
          <SignIn returnTo="/dashboard" />
        </SkyCanvasProvider>
      )
    }
  9. 9

    Protect server data

    UI components improve navigation, but authorization must be enforced where protected data is loaded.

    src/routes/protected.tsx
    import { createFileRoute, redirect } from "@tanstack/react-router"
    
    export const Route = createFileRoute("/protected")({
      beforeLoad: ({ context }) => {
        if (!context.skycanvasAuth.isAuthenticated) throw redirect({ to: "/login" })
      },
      component: ProtectedPage,
    })
  10. 10

    Register the app once in SkyCanvas

    Add your app origin and callback in SkyCanvas. Do not add this callback—or each consumer domain—to Google, GitHub, Facebook, or LinkedIn; those providers keep pointing only to New SSO.

    SkyCanvas dashboard (not a project file)
    Allowed origin: http://localhost:3000
    Callback URL:  http://localhost:3000/auth/callback

React SDK

Components, hooks, and ready-made account UI

These APIs are available below SkyCanvasProvider in standalone integrations. Better Auth integrations keep using Better Auth's own session hooks and components.

<SkyCanvasProvider />

Connect a React-only app with its publishable key and SSO URL. Full-stack apps instead give SsoProvider its server bootstrap.

<SignIn /> / <SignUp />

Ready-made embedded password, magic-link, and OAuth forms. Only OAuth buttons open provider popups.

<SsoAuth /> / <SsoAuthDialog />

The configurable auth form and its dialog wrapper when you need a custom sign-in or sign-up entry point.

<SsoSignInButton />

A small ready-made sign-in button for a header, hero, or any place a full auth form does not fit.

<SignedIn /> / <SignedOut />

Conditionally render UI after the provider has resolved authentication state.

useAuth()

Read isLoaded, isSignedIn, userId, session, getToken, and signOut.

useUser()

Read the verified SkyCanvas user and loading/authentication state.

<UserProfile />

One complete profile component with dialog and content modes, read-only OAuth avatar, email capability warnings, connected accounts, and active sessions.

<SsoUserMenu />

Ready-made account menu that opens UserProfile in dialog mode, supports custom links, and handles logout.

createSsoAccessTokenVerifier()

Cache public metadata/JWKS and verify React-only Bearer tokens in an application API.

Add a complete account menu and profile

UserProfile is the single profile implementation. SsoUserMenu opens it in dialog mode, while a profile route can render the same component in content mode. OAuth images stay read-only, account deletion is intentionally absent, and email actions clearly explain when the application needs a connected mail provider.

src/components/account-menu.tsx
import {
  SignIn,
  SignedIn,
  SignedOut,
  SsoUserMenu,
  UserProfile,
} from "@skycanvasstudio/sso/react"

export function AccountMenu() {
  return (
    <>
      <SignedOut>
        <SignIn returnTo="/dashboard" />
      </SignedOut>
      <SignedIn>
        <SsoUserMenu
          items={[{ label: "Dashboard", href: "/dashboard" }]}
          logoutReturnTo="/"
        />
        <UserProfile mode="content" />
      </SignedIn>
    </>
  )
}

// Or use <UserProfile mode="dialog" label={<ProfileIcon />} />.
// The label accepts text, an icon, or both.
// additionalContent can add one minimal app-specific section.

SignedIn protects presentation, not data. Always enforce authentication again in the server loader, route handler, or API that returns protected information.

Agent handoff

Give any coding agent the complete context

Copy this file into the target project or paste it into an agent task. It tells the agent to use the package, select one auth path, preserve the existing session system, and verify the result.

sso-agent-guide.md
# SkyCanvas SSO implementation guide for coding agents

Use the published `@skycanvasstudio/sso` package as a normal consumer. Do not
import package source files or copy OAuth, cookie, or token-verification logic
into the application.

## First choose one session owner

1. **React-only public client**: the SkyCanvas browser SDK owns the local
   short-lived token session; the application API verifies Bearer tokens.
2. **Better Auth**: Better Auth owns users, accounts, callback handling, cookies,
   and sessions.
3. **Another auth library**: that library owns the callback and session; use
   SkyCanvas provider metadata only.
4. **Full-stack without an auth library**: `createSsoServer` owns the OAuth flow and encrypted local
   application session.
5. **Non-JavaScript backend**: use a maintained OAuth 2.0/OIDC library; do not
   install the npm package.

Never combine Better Auth session hooks with the standalone `SsoProvider`.

## Environment rule

Session secrets and full-stack configuration belong in the application's
server-only env module. Pass explicit values to SkyCanvas exactly once and never
pass the complete environment object. A React-only public client intentionally
uses `VITE_SKYCANVAS_PUBLISHABLE_KEY` and `VITE_SKYCANVAS_SSO_URL`; the
publishable key is an identifier, not a secret.

## React-only path

Use this for a Vite/SPA application that should not run an auth callback server.
Register the exact frontend origin and `{APP_ORIGIN}/auth/callback`. The host
must serve the SPA entry at `/auth/callback`.

```tsx
import { SkyCanvasProvider } from "@skycanvasstudio/sso/react"
import "@skycanvasstudio/sso/styles.css"

<SkyCanvasProvider
  publishableKey={import.meta.env.VITE_SKYCANVAS_PUBLISHABLE_KEY}
  ssoUrl={import.meta.env.VITE_SKYCANVAS_SSO_URL}
>
  <App />
</SkyCanvasProvider>
```

Use `SignIn`, `SignedIn`, `SignedOut`, `useAuth`, and `useUser`. Send the result
of `useAuth().getToken()` as a Bearer token only to the intended application
API. Create one `createSsoAccessTokenVerifier()` in that API process and verify
each protected request. Treat `SignedIn` as presentation control, not backend
authorization. Do not add `createSsoServer`, Elysia, a client secret, or a second
callback to this path.

Use the same packaged profile UI in either form:

```tsx
import { UserProfile } from "@skycanvasstudio/sso/react"

<UserProfile mode="dialog" label="Profile" />
<UserProfile mode="content" />
```

The dialog `label` accepts any React node, including text, an icon, or both.
`UserProfile` keeps the OAuth avatar read-only and provides name updates, email
verification, password set/reset when enabled, connected-account management,
and active-session controls. It has no account-deletion action. Use the single
`additionalContent` slot only for a small application-specific section.

Profile requests use the same short-lived, application-scoped access token.
The browser never receives OAuth-provider credentials, mail-provider secrets,
or file-server credentials. If the application has no active mail connection,
the profile shows a warning and disables verification/password-email actions.

`SignIn` must remain embedded and show the password, magic-link, and social
methods returned by the application's public metadata. Password and magic-link
forms call central SkyCanvas directly. Only social provider buttons open a
popup, with the selected provider passed so the popup continues into that
provider rather than showing a second generic SkyCanvas login page.

## Better Auth path

Before SkyCanvas work, verify Better Auth installation, database adapter,
generated schema, migrations, server handler, browser client, and normal
sign-in/session behavior using Better Auth's official documentation.

Server configuration (`src/lib/auth.ts`):

```ts
import { skycanvas } from "@skycanvasstudio/sso/better-auth"
import { betterAuth } from "better-auth"
import { env } from "./env.server"

export const auth = betterAuth({
  // Preserve the existing database, plugins, and options.
  account: { encryptOAuthTokens: true },
  plugins: [skycanvas({
    publishableKey: env.SKYCANVAS_PUBLISHABLE_KEY,
    ssoUrl: env.SKYCANVAS_SSO_URL,
  })],
})
```

For TanStack Start, `tanstackStartCookies()` must remain the final plugin in
the array. The SSO server uses a distinct cookie prefix, so a local service app
and local SSO server cannot overwrite each other's OAuth state cookies merely
because both use the `localhost` hostname.

Mount `auth.handler` only through Better Auth's normal framework route. Register
exactly `{BETTER_AUTH_URL}/api/auth/oauth2/callback/skycanvas`. Do not create a
second callback or standalone `createSsoServer` instance.

Browser integration (`src/lib/auth-client.ts`):

```ts
import { skycanvasClient } from "@skycanvasstudio/sso/better-auth"
import { createAuthClient } from "better-auth/react"

export const authClient = createAuthClient({ plugins: [skycanvasClient()] })

export const signInWithSkyCanvas = (callbackURL = "/dashboard") =>
  authClient.signIn.oauth2({ providerId: "skycanvas", callbackURL })
```

Keep using Better Auth's existing session hooks, provider, user types, route,
and sign-out behavior. Do not add a second SkyCanvas provider or bootstrap
layer to the React tree.

## Another auth library

Call `createSsoProvider({ publishableKey: env.SKYCANVAS_PUBLISHABLE_KEY, ssoUrl: env.SKYCANVAS_SSO_URL })`
on the server and map its endpoints into the existing library. The library must
use Authorization Code, PKCE S256, state, nonce, server-side token exchange,
JWKS signature verification, issuer, audience, expiry, and subject validation.
Keep its own callback, session, user types, and logout behavior.

## Standalone path

For TanStack Start, configure the adapter once:

```ts
import { createTanStackSso } from "@skycanvasstudio/sso/tanstack-start"
import { env } from "./env.server"

export const skycanvas = createTanStackSso({
  publishableKey: env.SKYCANVAS_PUBLISHABLE_KEY,
  secretKey: env.SKYCANVAS_SECRET_KEY,
  ssoUrl: env.SKYCANVAS_SSO_URL,
})
```

Mount its middleware once:

```ts
import { createServerOnlyFn, createStart } from "@tanstack/react-start"
import { createTanStackSsoMiddleware } from "@skycanvasstudio/sso/tanstack-start"

const load = createServerOnlyFn(() =>
  import("./lib/skycanvas.server").then(({ skycanvas }) => skycanvas),
)

export const startInstance = createStart(() => ({
  requestMiddleware: [createTanStackSsoMiddleware(load)],
}))
```

For Next.js use `createNextSso()` with the same three values and export its
`GET`, `POST`, and `OPTIONS` handlers from `app/auth/[...sso]/route.ts`. The SDK
infers the public app origin and `/auth/callback` URL from the request. Set
`appUrl` only when a proxy does not forward the original host and protocol.

Mount `SkyCanvasProvider` once and use the packaged `SignIn`, `SignedIn`,
`SignedOut`, `useAuth`, `SsoUserMenu`, and `UserProfile` APIs. Register
`{APP_ORIGIN}/auth/callback` in SkyCanvas.

`SsoUserMenu` uses `UserProfile mode="dialog"` internally. Render
`<UserProfile mode="content" />` on a dedicated profile page, or render
`<UserProfile mode="dialog" label={...} />` for a custom trigger. The component
provides the same read-only OAuth avatar, name, verification, password,
connected-account, active-session, mail-capability, and minimal extension
behavior described in the React-only path. It intentionally excludes account
deletion and avatar upload.

## User webhooks for full-stack applications

User webhooks are optional application-level delivery configuration. They are
not OAuth client configuration and are not passed to `createNextSso()`,
`createTanStackSso()`, or the `/auth/[...sso]` route. Configure one endpoint
per SkyCanvas application with `PUT /admin/applications/:applicationId/webhooks`.
The response returns the generated secret only when the endpoint is first
created or rotated; store it as server-only `SSO_WEBHOOK_SECRET` in that
application.

Mount a separate `POST` route such as `/api/sso/webhooks`; never put it inside
`app/auth/[...sso]/route.ts`. Use `createWebhookHandler()` from
`@skycanvasstudio/sso/server`, keep receiver changes idempotent with an upsert
using a unique `ssoUserId`, and delete by that column for `user.deleted`.

```ts
import { createWebhookHandler } from "@skycanvasstudio/sso/server"

export const POST = createWebhookHandler(
  {
    "user.created": ({ data }) => prisma.user.upsert(/* keyed by data.id */),
    "user.updated": ({ data }) => prisma.user.upsert(/* keyed by data.id */),
    "user.deleted": ({ data }) => prisma.user.deleteMany({ where: { ssoUserId: data.id } }),
  },
  { secret: process.env.SSO_WEBHOOK_SECRET! },
)
```

Webhooks are eventually delivered, so add `onSignIn` to the full-stack SDK
configuration as a repair path. It receives the verified SSO user before the
local session is written; upsert the same `ssoUserId` there. This repairs a
client database user that is missing because a prior webhook or local write
failed.

The standalone handler must receive both `GET` and `POST /auth/user-profile`.
Mounting the documented `/auth/*` catch-all or the Next/TanStack adapters does
this automatically. The adapter keeps the app access token sealed in the
application's encrypted HttpOnly cookie and proxies profile operations to
SkyCanvas; do not expose that token through bootstrap data or a JSON session
endpoint. After upgrading an existing integration to a version that adds this
route, sign in once again so the local session contains the required profile
authorization data.

## Required verification

- Missing values and invalid URLs produce actionable configuration errors.
- The bootstrap is plain serializable data and contains no secrets or functions.
- A returning SSR session renders authenticated without a flash or initial
  profile request.
- New-user login, returning-user login, callback rejection, protected routes,
  local/global logout, and safe local return paths work.
- TanStack never imports or returns the SSO server object through a server
  function; only the bootstrap crosses the boundary.
- Better Auth, generic-library, and full-stack standalone flows keep OAuth
  tokens, flow state, nonce, verifier, and session secrets server-only.
- The React-only flow keeps PKCE state and the short-lived application token in
  the SDK-managed browser session; protected APIs verify every Bearer token and
  no session secret exists in the frontend.
- `UserProfile` works in both dialog and content modes; full-stack profile
  requests reach `GET` and `POST /auth/user-profile` without returning the
  sealed access token to React.
- OAuth avatars remain read-only, account deletion is absent, and email-driven
  actions are disabled with a clear warning when the application has no active
  mail-provider connection.
- Webhook endpoints are configured once per SkyCanvas application, not in an
  OAuth client or browser configuration. The receiver route is separate from
  `/auth/[...sso]`, verifies `SSO_WEBHOOK_SECRET`, handles every event
  idempotently, and `onSignIn` repairs a missed delivery.

Common problems

Troubleshooting

redirect_uri is invalid or login returns 403

Copy the exact callback URL from the selected guide into the application client. Paths, ports, schemes, and trailing slashes must match.

React popup signs in but the opener never updates

Make /auth/callback load the same SPA entry and register the frontend origin. Do not proxy that route to a removed local auth server.

Token exchange fails from a React app

Add the exact frontend origin to Allowed origins and confirm OAuth token issuance is enabled on the SkyCanvas deployment.

Session disappears after callback in a full-stack app

Use HTTPS in production, keep appUrl equal to the public app origin, and verify proxy forwarded host/protocol headers and cookie SameSite settings.

Hooks say the provider is missing

Mount exactly one provider above the route tree and use hooks from the same integration path. Do not mix Better Auth-generated hooks with standalone hooks.

Protected UI works but API requests are still public

SignedIn only controls rendering. Verify the Bearer token in a React-only app API, or read the verified server session in a full-stack app before returning protected data.

Before production

Security checklist

  • Choose one integration path and do not create a second session system.
  • Register the exact callback URL produced by your selected integration path.
  • Use HTTPS for every production app, API, callback, and SSO origin.
  • Keep session secrets server-only; never put them in VITE_ or NEXT_PUBLIC_ variables.
  • Use an HttpOnly application cookie for full-stack apps; use only short-lived verified tokens in React-only apps.
  • Validate issuer, audience, signature, expiry, and subject on every protected API token.
  • Allow only relative return paths and use exact allowed origins.
  • Test new users, returning users, logout, invalid callbacks, and protected routes.
Create an application