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.
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 }.
| Mutation | Arguments | Returns |
|---|---|---|
register | input: { modelSlug, identity, password, fields } | { success, message } |
login | input: { modelSlug, identity, password } | { success, message, accessToken, refreshToken, accessTokenExpiresIn } |
refresh | refreshToken | { success, message, accessToken, refreshToken, accessTokenExpiresIn } |
logout | refreshToken | { success, message } |
logoutEverywhere | - (needs Bearer) | { success, message } |
forgotPassword | modelSlug, identity | { success, message } |
resetPassword | token, newPassword | { success, message } |
verifyEmail | token | { success, message } |