Guides
Members
Sign in and recover accounts

Sign in and recover accounts

Requires: A secret key with MEMBER_AUTH, called from your server.

Every route here needs a secret key with MEMBER_AUTH, so every call runs on your server. A publishable key answers 403 SECRET_KEY_REQUIRED.

POST /api/v1/communities/:communityTag/auth/sign-in
POST /api/v1/communities/:communityTag/auth/sign-up
POST /api/v1/communities/:communityTag/auth/email-link
POST /api/v1/communities/:communityTag/auth/email-link/verify
GET  /api/v1/communities/:communityTag/auth/me
POST /api/v1/communities/:communityTag/auth/password-reset
GET  /api/v1/communities/:communityTag/auth/password-reset/validate
POST /api/v1/communities/:communityTag/auth/password-reset/confirm
POST /api/v1/communities/:communityTag/auth/verify-email
POST /api/v1/communities/:communityTag/me/verify-email/resend
POST /api/v1/communities/:communityTag/auth/oauth/exchange

At a glance

curl -X POST "https://api.cobuntu.com/api/v1/communities/your-community/auth/sign-in" \
  -H "X-API-Key: $COBUNTU_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"alex@example.com","password":"correct horse battery"}'
# { "token": "...", "user": { ... }, "newMember": false }

The session

Every sign-in path ends in the same answer:

{ "token": "eyJhbGciOi...", "user": { "id": "usr_123", "name": "Alex Doe", "email": "alex@example.com", "avatar": null }, "newMember": false }

token is a 30-day session bound to this one community. Store it in your own cookie and never expose it to page scripts:

cookies().set("member_session", token, {
  httpOnly: true, secure: true, sameSite: "lax", path: "/", maxAge: 60 * 60 * 24 * 30,
});
const member = cobuntu.as(token); // sends X-Cobuntu-Auth-Token

On 401 INVALID_SESSION from any member call, clear the cookie and send the visitor to sign in.

Sign in and sign up

const s = await cobuntu.auth.signIn({ email, password });
const s2 = await cobuntu.auth.signUp({ name, email, password }, { endUserIp: visitorIp });
CodeShow
INVALID_CREDENTIALS"Email or password is incorrect."
SUSPENDEDThe account is suspended.
APPLICATION_PENDINGTheir application is still under review.
NOT_MEMBERNot a member: offer the apply form.
WEAK_PASSWORD, EMAIL_TAKENSign-up form errors.
APPLICATION_REQUIREDThe community reviews applications: use the apply flow.
RATE_LIMITEDToo many attempts.

Signing in to an open community joins it (newMember: true). Pass endUserIp on sign-up: its limit is per visitor IP, and every call comes from your server.

Sign-in links

await cobuntu.auth.emailLink({ email, redirectPath: "/account" }); // always { ok: true }

The answer never reveals whether the account exists. The email links to your site with a token (valid 15 minutes); exchange it on your server:

const s = await cobuntu.auth.verifyEmailLink(token); // LINK_EXPIRED, INVALID_LINK

Who is signed in

const me = await cobuntu.as(session).auth.me();
// { user: {...}, member: true, segments: [{ id: "seg_pro", name: "Pro" }] }

member is true for an accepted membership; segments are the active tiers.

Password recovery

// 1. Request: always { ok: true }
await cobuntu.auth.passwordReset(email);
 
// 2. The email links to your page with ?token=. Check it before showing the form.
await cobuntu.auth.validatePasswordReset(token); // { valid: true }, does not consume
 
// 3. Set the new password. Consumes the token and returns a fresh session.
const s = await cobuntu.auth.confirmPasswordReset({ token, password });

Codes: LINK_EXPIRED, INVALID_LINK, WEAK_PASSWORD, RATE_LIMITED (3 requests per 15 minutes per email).

Email verification

The verification email links to your site with a token:

await cobuntu.auth.verifyEmail(token); // { verified: true }; LINK_EXPIRED, INVALID_LINK, EMAIL_TAKEN

A signed-in member can ask for a new one:

await cobuntu.as(session).me.verifyEmailResend(); // { sent: true }; ALREADY_VERIFIED, RATE_LIMITED

Continue with Google

Two routes on your site and a PKCE pair. The verifier stays on your server.

import { createPkcePair } from "@cobuntu/embed";
 
// GET /api/auth/google/start
const { codeVerifier, codeChallenge } = await createPkcePair();
const state = (await createPkcePair()).codeVerifier;
cookies().set("google_oauth", `${state}.${codeVerifier}`, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 600 });
return redirect(cobuntu.auth.googleStartUrl({
  returnUrl: `${origin}/api/auth/google/callback`,
  codeChallenge,
  state,
}));
 
// GET /api/auth/google/callback?code=&state=   (or ?error=)
const [savedState, codeVerifier] = cookies().get("google_oauth")!.value.split(".");
cookies().delete("google_oauth");
if (searchParams.get("state") !== savedState) return redirect("/login?error=state");
if (searchParams.get("error")) return redirect(`/login?error=${searchParams.get("error")}`);
 
const { token, user, isSignUp } = await cobuntu.auth.exchangeOAuthCode({
  code: searchParams.get("code")!,
  codeVerifier,
});
  • returnUrl must be on the community's site origin (localhost is allowed while developing).
  • The code lives 60 seconds and works once. Every invalid, expired, reused or mismatched code, and a wrong verifier, answers the same 400 INVALID_CODE, so codes cannot be probed. Start the flow again.
  • 403 SUSPENDED for a suspended account.
  • isSignUp is true when this Google sign-in created the account; use it for a welcome step.
  • No membership is created and newMember is always false. Who may sign in follows the community's access rules; refusals come back to your callback as ?error= (for example NOT_MEMBER, APPLICATION_PENDING).