Skip to content

Auth Module

The Auth module provides user authentication, group management, role-based access control, and a flexible permission system that integrates with all other Cohera modules.

Built on better-auth, the Auth module extends its foundation with Cohera-specific plugins for groups, roles, and module-level permissions. This hybrid permission model combines:

  • Groups: Context-specific collections (forums, communities, organizations)
  • Roles: User positions within groups (admin, moderator, member, etc.)
  • Permissions: Module-declared actions that roles can perform

Configure the auth module in your cohera.config.js:

import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } from '$env/static/private'
export default {
modules: {
auth: {
// Authentication providers
providers: {
credentials: {
enabled: true,
requireEmailVerification: true,
},
oauth: {
google: {
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
},
github: {
clientId: GITHUB_CLIENT_ID,
clientSecret: GITHUB_CLIENT_SECRET,
},
},
},
// Multi-factor authentication
mfa: {
enabled: true,
required: false, // Enforce for all users
methods: ["totp", "sms"],
},
// Session management
session: {
expiresIn: "7d",
updateAge: "1d",
cookieName: "cohera.session",
},
// Default roles available in all groups (groups can override)
defaultRoles: [
{
name: "admin",
permissions: ["*"], // All permissions
},
{
name: "moderator",
permissions: [
"posts.delete",
"posts.edit",
"posts.moderate",
"users.moderate",
],
},
{
name: "member",
permissions: ["posts.create", "posts.edit.own"],
},
],
// Password requirements
password: {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: false,
},
},
},
};

Authentication provider configuration.

  • Type: object
  • Required: Yes

Email/password authentication settings.

  • Type: object
  • enabled (boolean): Enable credential-based auth
  • requireEmailVerification (boolean): Require email verification before login

OAuth provider configurations. Each provider requires clientId and clientSecret. All providers supported by better-auth can be provided and configured. See google for an example.

  • Type: object

Multi-factor authentication settings.

  • Type: object
  • enabled (boolean): Enable MFA support
  • required (boolean): Require MFA for all users
  • methods (array): Available MFA methods - ['totp', 'email']
    • totp: a one time password generated by a separate authenticator app (for example a password manager).
    • email: a one time code is sent to the users email.

Session management configuration. This controls how long users stay logged in. Optional configuration with sane defaults.

  • Type: object
  • expiresIn (string): Session lifetime (e.g., '7d', '24h')
  • updateAge (string): How often to update session expiry
  • cookieName (string): Session cookie name

Platform-wide roles automatically available in all groups. Groups can override these by defining a role with the same name.

  • Type: array<object>
  • Each role object:
    • name (string): Role identifier
    • permissions (array): Array of permission strings, or ['*'] for all permissions

Password validation requirements.

  • Type: object
  • minLength (number): Minimum password length
  • requireUppercase (boolean): Require uppercase letters
  • requireLowercase (boolean): Require lowercase letters
  • requireNumbers (boolean): Require numbers
  • requireSpecialChars (boolean): Require special characters

The Auth module uses a hybrid permission system where:

  1. Modules declare permissions in their config (e.g., posts.create, posts.moderate)
  2. Roles grant permissions to users within specific groups
  3. Middleware validates permissions before allowing actions
// User Alice in "Gaming" forum
{
userId: "alice",
groupId: "gaming-forum",
role: ["moderator"] // Has posts.delete permission
}
// Same user Alice in "Cooking" forum
{
userId: "alice",
groupId: "cooking-forum",
roles: [] // empty roles == base member role
}

When Alice tries to delete a post, the middleware checks:

hasPermission(alice, post.groupId, "posts.delete");
// → true in gaming-forum
// → false in cooking-forum

Groups represent context-specific collections where users have different roles:

interface Group {
id: string;
parent?: Group;
name: string;
slug: string;
description?: string;
visibility: "public" | "private" | "secret";
createdAt: Date;
updateAt: Date;
}

Groups can contain subgroups, enabling hierarchical structures like organizations, departments, and teams. The parent field in the Group interface references the parent group.

Key behaviors:

  • Creator becomes admin: When creating a subgroup, the creator is automatically added as the initial member with the admin role
  • No membership inheritance: Users in a parent group are NOT automatically members of subgroups - they must be explicitly added
  • No role inheritance: A user’s role in a parent group does NOT carry over to subgroups
  • Default roles apply: Platform-wide defaultRoles are available in all groups unless explicitly overridden
// Top-level organization
{
id: "acme-corp",
name: "Acme Corporation",
slug: "acme",
parent: undefined
}
// Department subgroup
{
id: "engineering",
name: "Engineering",
slug: "engineering",
parent: { id: "acme-corp", ... }
}
// Team subgroup
{
id: "frontend-team",
name: "Frontend Team",
slug: "frontend",
parent: { id: "engineering", ... }
}

In this example, a user who is an admin of “Acme Corporation” does NOT automatically have any role in “Engineering” or “Frontend Team”. They must be explicitly added to each subgroup where they need access.

Users are assigned roles within specific groups:

interface GroupMembership {
userId: string;
groupId: string;
role: string; // References role name from config
joinedAt: Date;
}

Modules declare required permissions in their config:

// posts module config
export default {
permissions: [
{
name: "posts.create",
description: "Create new posts",
},
{
name: "posts.edit",
description: "Edit any post",
},
{
name: "posts.edit.own",
description: "Edit own posts",
},
{
name: "posts.delete",
description: "Delete posts",
},
{
name: "posts.moderate",
description: "Moderate posts (feature, pin, etc.)",
},
],
};

Create a new user account.

Usage:

Terminal window
cohera auth user create <email> [options]

Flags:

  • --password <password> - Set initial password
  • --role <role> - Assign global role
  • --verified - Mark email as verified
  • --admin - Create as admin user

Examples:

Terminal window
# Create user with interactive prompts
cohera auth user create alice@example.com
# Create verified admin user
cohera auth user create admin@example.com --verified --admin
# Create user with specific password
cohera auth user create bob@example.com --password "SecurePass123"

Delete a user account.

Usage:

Terminal window
cohera auth user delete <email|user-id>

Flags:

  • --force - Skip confirmation prompt
  • --keep-content - Delete user but preserve their content

Examples:

Terminal window
# Delete user with confirmation
cohera auth user delete alice@example.com
# Force delete without confirmation
cohera auth user delete alice@example.com --force

Create a new group or subgroup.

Usage:

Terminal window
cohera auth group create <name> [options]

Flags:

  • --slug <slug> - URL-friendly identifier
  • --parent <group-slug> - Create as subgroup of existing group
  • --visibility <public|private|secret> - Group visibility (default: public)
  • --description <text> - Group description

Examples:

Terminal window
# Create top-level group
cohera auth group create "Gaming Community" --slug gaming
# Create subgroup
cohera auth group create "Speedrunners" --slug speedrunners --parent gaming
# Create private subgroup with description
cohera auth group create "Core Team" --slug core --parent gaming --visibility private --description "Invitation only"

The user running this command is automatically added as the initial admin of the created group.

Add a user to a group with a specific role.

Usage:

Terminal window
cohera auth group add-member <group-slug> <user-email> --role <role>

Flags:

  • --role <role> - Role to assign (required)

Examples:

Terminal window
# Add user as moderator
cohera auth group add-member gaming-forum alice@example.com --role moderator
# Add user as member
cohera auth group add-member cooking-forum alice@example.com --role member

Remove a user from a group.

Usage:

Terminal window
cohera auth group remove-member <group-slug> <user-email>

Examples:

Terminal window
# Remove user from group
cohera auth group remove-member gaming-forum alice@example.com

Create a new role definition.

Usage:

Terminal window
cohera auth role create <role-name> [permissions...]

Flags:

  • --description <text> - Role description

Examples:

Terminal window
# Create content editor role
cohera auth role create editor posts.create posts.edit posts.moderate
# Create role with description
cohera auth role create contributor "posts.create" --description "Community contributors"

Check if a user has a specific permission in a group context.

function hasPermission(
userId: string,
groupId: string,
permission: string,
): Promise<boolean>;

Parameters:

  • userId - The user to check
  • groupId - The group context
  • permission - Permission string to validate (e.g., 'posts.delete')

Returns: true if user has permission, false otherwise

Example:

import { hasPermission } from "@cohera/auth/api";
// Check before allowing deletion
if (await hasPermission(userId, post.groupId, "posts.delete")) {
await deletePost(postId);
} else {
throw new Error("Insufficient permissions");
}

Middleware helper that throws an error if permission check fails.

function requirePermission(
userId: string,
groupId: string,
permission: string,
): Promise<void>;

Parameters:

  • userId - The user to check
  • groupId - The group context
  • permission - Required permission string

Throws: UnauthorizedError if user lacks permission

Example:

import { requirePermission } from '@cohera/auth/api'
// In a tRPC procedure
async deletePost(postId: string) {
const post = await db.posts.findById(postId)
// Will throw if user lacks permission
await requirePermission(ctx.userId, post.groupId, 'posts.delete')
await db.posts.delete(postId)
}

Get all groups a user belongs to with their roles.

function getUserGroups(userId: string): Promise<GroupMembership[]>;
interface GroupMembership {
groupId: string;
groupName: string;
groupSlug: string;
role: string;
permissions: string[];
joinedAt: Date;
}

Example:

import { getUserGroups } from "@cohera/auth/api";
const groups = await getUserGroups(userId);
// Find groups where user is admin
const adminGroups = groups.filter((g) => g.role === "admin");

Get all members of a group with their roles.

function getGroupMembers(groupId: string): Promise<Member[]>;
interface Member {
userId: string;
email: string;
name?: string;
role: string;
permissions: string[];
joinedAt: Date;
}

Example:

import { getGroupMembers } from "@cohera/auth/api";
const members = await getGroupMembers(groupId);
// Get all moderators
const moderators = members.filter((m) => m.role === "moderator");

Get immediate subgroups of a group.

function getSubGroups(groupId: string): Promise<Group[]>;

Parameters:

  • groupId - The parent group ID

Returns: Array of immediate child groups (does not include nested descendants)

Example:

import { getSubGroups } from "@cohera/auth/api";
const subgroups = await getSubGroups("acme-corp");
// Returns: [{ id: "engineering", name: "Engineering", ... }, { id: "marketing", ... }]

Get the parent chain of a group, from immediate parent to root.

function getGroupAncestors(groupId: string): Promise<Group[]>;

Parameters:

  • groupId - The group to find ancestors for

Returns: Array of ancestor groups, ordered from immediate parent to root. Empty array if group has no parent.

Example:

import { getGroupAncestors } from "@cohera/auth/api";
const ancestors = await getGroupAncestors("frontend-team");
// Returns: [
// { id: "engineering", name: "Engineering", ... }, // immediate parent
// { id: "acme-corp", name: "Acme Corporation", ... } // root
// ]

Other modules use auth middleware to protect routes and check permissions:

src/hooks.server.ts
import { authMiddleware } from "@cohera/auth/middleware";
export const handle = authMiddleware({
// Make user available in all routes
attachUser: true,
// Redirect unauthenticated users
redirectTo: "/login",
// Paths that don't require auth
publicPaths: ["/", "/about", "/login", "/signup"],
});

In route handlers:

src/routes/groups/[slug]/posts/[id]/delete/+server.ts
import { requirePermission } from "@cohera/auth/api";
import type { RequestHandler } from "./$types";
export const DELETE: RequestHandler = async ({ params, locals }) => {
const { id, slug } = params;
const userId = locals.user.id;
const post = await db.posts.findById(id);
// Check permission before deletion
await requirePermission(userId, post.groupId, "posts.delete");
await db.posts.delete(id);
return new Response(null, { status: 204 });
};