Skip to main content

Session-Based Authentication

Featul uses better-auth for session-based authentication. All authenticated requests include session cookies that are validated on the server.

Authentication Methods

  • Email/Password with OTP verification
  • Google OAuth
  • GitHub OAuth
  • Cross-subdomain cookies for multi-tenant workspaces

Procedure Types

All API endpoints are defined as either public or private procedures:

Public Procedures

Public procedures can be called without authentication:
// Anyone can fetch workspace info
const res = await client.workspace.bySlug.$get({ 
  slug: "acme" 
})
Examples of public endpoints:
  • workspace.bySlug - Get workspace details
  • board.byWorkspaceSlug - List public boards
  • post.create - Create a post (may allow anonymous)
  • post.vote - Vote on a post (may allow anonymous with fingerprint)
  • comment.list - List comments on a post

Private Procedures

Private procedures require authentication and will return a 401 error if the session is invalid:
// Requires authentication
const res = await client.workspace.create.$post({
  name: "ACME Inc",
  slug: "acme",
  domain: "https://acme.com",
  timezone: "America/New_York"
})

if (res.status === 401) {
  // User needs to sign in
  console.error("Unauthorized")
}
Examples of private endpoints:
  • workspace.create - Create a workspace
  • workspace.update* - Update workspace settings
  • board.settingsByWorkspaceSlug - Get board settings
  • post.update - Update a post
  • post.delete - Delete a post
  • comment.update - Update a comment

Authentication Flow

The authentication flow is handled automatically:
  1. User signs in via better-auth
  2. Session cookie is set (works across subdomains)
  3. Client automatically includes cookie in all requests
  4. Server validates session in authMiddleware
  5. Session data is available in ctx.session

Server-Side Session Access

// In a private procedure
export const privateProcedure = baseProcedure.use(authMiddleware)

const myEndpoint = privateProcedure
  .input(schema)
  .post(async ({ ctx, input, c }) => {
    // Access authenticated user
    const userId = ctx.session.user.id
    const userEmail = ctx.session.user.email
    
    // Your logic here
  })

Anonymous Operations

Some operations support anonymous users with fingerprinting:

Anonymous Posting

Boards can allow anonymous posts if configured:
const res = await client.post.create.$post({
  title: "Feature request",
  content: "Please add this feature",
  workspaceSlug: "acme",
  boardSlug: "features",
  fingerprint: "unique-browser-fingerprint" // For anonymous users
})

Anonymous Voting

Voting works for both authenticated and anonymous users:
// Authenticated user - userId from session
await client.post.vote.$post({ 
  postId: "post-123"
})

// Anonymous user - requires fingerprint
await client.post.vote.$post({ 
  postId: "post-123",
  fingerprint: "unique-browser-fingerprint"
})

Permission Levels

Authenticated users have different permission levels within workspaces:

Workspace Owner

  • Full control over workspace
  • Can delete workspace
  • Can manage billing
  • All permissions below

Admin

  • Can manage members
  • Can configure boards
  • Can moderate all content
  • All permissions below

Member

  • Can create posts
  • Can comment
  • Can vote
  • Limited moderation

Viewer

  • Can view content
  • Can vote (if board allows)
  • Cannot create or edit

Error Handling

try {
  const res = await client.workspace.delete.$post({
    slug: "acme",
    confirmName: "ACME Inc"
  })
  
  if (!res.ok) {
    if (res.status === 401) {
      // Not authenticated
    } else if (res.status === 403) {
      // Authenticated but not authorized
    } else if (res.status === 404) {
      // Resource not found
    }
  }
} catch (error) {
  console.error("Network error", error)
}

Session Management

Sessions are managed by better-auth:
import { auth } from "@featul/auth"

// Server-side: Get session
const session = await auth.api.getSession({ 
  headers: request.headers 
})

if (session?.user) {
  console.log(session.user.id)
  console.log(session.user.email)
}

Build docs developers (and LLMs) love