Now with AI-powered page building via MCP Server

Member Authentication

Authenticate site members through the backend siteMember GraphQL mutations via the gateway - register, login, refresh, logout, password reset and email verification. The slim SDK ships no auth helpers; you own the session.

Last updated: July 24, 2026

Overview

Site members are the end-users of your published site - the people who register, sign in and own an account. A member is a record of a content model in your workspace, identified by its modelSlug (for example members) plus an identity (usually an email). There is no special user table - members are records.

Member authentication is a live backend feature, exposed through the siteMember GraphQL mutation namespace. You call it through the gateway with createCmssyClient(cmssy).query(). The slim SDK ships no auth helpers - no auth route, no middleware, no session reader. You own the flow and you own the session.

You own the session. login and refresh return the raw accessToken and refreshToken - the backend sets no cookie. Your app decides where they live (an httpOnly cookie your route sets is the safe default) and attaches Authorization: Bearer <accessToken> on authenticated calls.


1. The gateway client

Create one createCmssyClient instance and keep every mutation document beside it. Each mutation lives under the siteMember namespace.

// lib/cmssy-members.ts
import { createCmssyClient } from "@cmssy/react";
import { cmssy } from "@/cmssy.config";

// One gateway client. Every siteMember mutation goes through client.query().
export const client = createCmssyClient(cmssy);

export const REGISTER = `mutation Register($input: SiteMemberRegisterInput!) {
  siteMember { register(input: $input) { success message } }
}`;

export const LOGIN = `mutation Login($input: SiteMemberLoginInput!) {
  siteMember {
    login(input: $input) {
      success
      message
      accessToken
      refreshToken
      accessTokenExpiresIn
    }
  }
}`;

export const REFRESH = `mutation Refresh($refreshToken: String!) {
  siteMember {
    refresh(refreshToken: $refreshToken) {
      success
      message
      accessToken
      refreshToken
      accessTokenExpiresIn
    }
  }
}`;

export const LOGOUT = `mutation Logout($refreshToken: String!) {
  siteMember { logout(refreshToken: $refreshToken) { success message } }
}`;

export const LOGOUT_EVERYWHERE = `mutation LogoutEverywhere {
  siteMember { logoutEverywhere { success message } }
}`;

export const FORGOT_PASSWORD = `mutation ForgotPassword($modelSlug: String!, $identity: String!) {
  siteMember { forgotPassword(modelSlug: $modelSlug, identity: $identity) { success message } }
}`;

export const RESET_PASSWORD = `mutation ResetPassword($token: String!, $newPassword: String!) {
  siteMember { resetPassword(token: $token, newPassword: $newPassword) { success message } }
}`;

export const VERIFY_EMAIL = `mutation VerifyEmail($token: String!) {
  siteMember { verifyEmail(token: $token) { success message } }
}`;

2. Register

register creates a member record from modelSlug, identity and password; extra profile fields go in fields. It returns { success, message } and, when email verification is required, sends a verification email.

// app/api/auth/register/route.ts
import { client, REGISTER } from "@/lib/cmssy-members";

export async function POST(request: Request) {
  const { email, password, name } = await request.json();
  const { siteMember } = await client.query(REGISTER, {
    input: {
      modelSlug: "members",
      identity: email,
      password,
      fields: { name },
    },
  });
  return Response.json(siteMember.register); // { success, message }
}

3. Log in and store the session

login returns { success, message, accessToken, refreshToken, accessTokenExpiresIn }. The backend sets no cookie - your route stores the tokens. Below they go into httpOnly cookies your app controls.

// app/api/auth/login/route.ts
import { cookies } from "next/headers";
import { client, LOGIN } from "@/lib/cmssy-members";

export async function POST(request: Request) {
  const { email, password } = await request.json();
  const { siteMember } = await client.query(LOGIN, {
    input: { modelSlug: "members", identity: email, password },
  });
  const res = siteMember.login;
  if (!res.success || !res.accessToken) {
    return Response.json({ ok: false, message: res.message }, { status: 401 });
  }
  // You own the session - store the raw tokens yourself. The backend sets no cookie.
  const jar = await cookies();
  jar.set("member_access", res.accessToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    maxAge: res.accessTokenExpiresIn ?? 900,
  });
  jar.set("member_refresh", res.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
  });
  return Response.json({ ok: true });
}

4. Authenticated requests

Any member-scoped operation needs the access token. Read it from your store and send it as Authorization: Bearer <accessToken>; the backend resolves it into the signed-in member. logoutEverywhere is one such call - it revokes every session for the member.

// app/api/auth/logout-everywhere/route.ts
import { cookies } from "next/headers";
import { client, LOGOUT_EVERYWHERE } from "@/lib/cmssy-members";

export async function POST() {
  const accessToken = (await cookies()).get("member_access")?.value;
  if (!accessToken) return Response.json({ ok: false }, { status: 401 });
  // Authenticated call: attach the access token as a Bearer header.
  const { siteMember } = await client.query(LOGOUT_EVERYWHERE, {}, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return Response.json(siteMember.logoutEverywhere);
}

5. Refresh

refresh exchanges a refreshToken for a fresh token pair (the same shape as login). Rotate both cookies with the new values; if it fails, clear the session.

// app/api/auth/refresh/route.ts
import { cookies } from "next/headers";
import { client, REFRESH } from "@/lib/cmssy-members";

export async function POST() {
  const jar = await cookies();
  const refreshToken = jar.get("member_refresh")?.value;
  if (!refreshToken) return Response.json({ ok: false }, { status: 401 });
  const { siteMember } = await client.query(REFRESH, { refreshToken });
  const res = siteMember.refresh;
  if (!res.success || !res.accessToken) {
    jar.delete("member_access");
    jar.delete("member_refresh");
    return Response.json({ ok: false }, { status: 401 });
  }
  // refresh rotates the pair - overwrite both cookies with the new tokens.
  jar.set("member_access", res.accessToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
    maxAge: res.accessTokenExpiresIn ?? 900,
  });
  jar.set("member_refresh", res.refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    path: "/",
  });
  return Response.json({ ok: true });
}

6. Log out

logout revokes a single refreshToken. Call it, then clear your own cookies.

// app/api/auth/logout/route.ts
import { cookies } from "next/headers";
import { client, LOGOUT } from "@/lib/cmssy-members";

export async function POST() {
  const jar = await cookies();
  const refreshToken = jar.get("member_refresh")?.value;
  if (refreshToken) {
    await client.query(LOGOUT, { refreshToken });
  }
  jar.delete("member_access");
  jar.delete("member_refresh");
  return Response.json({ ok: true });
}

7. Password reset

forgotPassword(modelSlug, identity) emails a reset link and always reports success, so no one can probe which accounts exist. resetPassword(token, newPassword) consumes the token from that link and sets the new password.

// forgot password - always reports success (no account enumeration)
await client.query(FORGOT_PASSWORD, { modelSlug: "members", identity: email });

// reset password - token comes from the emailed link (/reset-password?token=...)
await client.query(RESET_PASSWORD, { token, newPassword });

8. Email verification

verifyEmail(token) consumes the token from the verification link. When a workspace requires verification, sign-in stays blocked until the member is verified.

// verify email - token comes from the verification link (/verify-email?token=...)
await client.query(VERIFY_EMAIL, { token });

Mutation reference

Every mutation lives under the siteMember namespace. login and refresh return tokens; the rest return { success, message }.

MutationArgumentsReturns
registerinput: { modelSlug, identity, password, fields }{ success, message }
logininput: { modelSlug, identity, password }{ success, message, accessToken, refreshToken, accessTokenExpiresIn }
refreshrefreshToken{ success, message, accessToken, refreshToken, accessTokenExpiresIn }
logoutrefreshToken{ success, message }
logoutEverywhere- (needs Bearer){ success, message }
forgotPasswordmodelSlug, identity{ success, message }
resetPasswordtoken, newPassword{ success, message }
verifyEmailtoken{ success, message }