Skip to main content

Overview

The Inmobiliaria API uses session-based authentication with HTTP-only cookies. Sessions are automatically created upon successful authentication (sign-up, sign-in, or OAuth) and validated on subsequent requests.

Session Configuration

Sessions are configured in src/auth/index.ts:
session: {
  expiresIn: 60 * 60 * 24 * 30, // 30 days in seconds
  updateAge: 60 * 60 * 24 * 7,  // 7 days in seconds
}
expiresIn
number
Session expiration time: 30 days (2,592,000 seconds)
updateAge
number
Session refresh threshold: 7 days (604,800 seconds). If a session is older than this, it will be extended on the next request.

How Sessions Work

1. Session Creation

When a user successfully authenticates (via email/password or OAuth), Better Auth:
  1. Generates a unique session ID and token
  2. Stores session data in the sessions table
  3. Sets an HTTP-only cookie with the session token
  4. Returns user and session information
Set-Cookie: inmobiliaria_session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; 
  HttpOnly; 
  Secure; 
  SameSite=None; 
  Max-Age=2592000; 
  Path=/

2. Session Storage

Session data is stored in the PostgreSQL database:
CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  token TEXT NOT NULL UNIQUE,
  expires_at TIMESTAMP NOT NULL,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  ip_address TEXT,
  user_agent TEXT,
  created_at TIMESTAMP NOT NULL,
  updated_at TIMESTAMP NOT NULL
);

Session Table Schema

id
string
Unique session identifier
token
string
Encrypted session token (matches cookie value)
expiresAt
timestamp
When the session expires (30 days from creation)
userId
string
Reference to the authenticated user
ipAddress
string
IP address where session was created (optional)
userAgent
string
Browser user agent (optional)
createdAt
timestamp
Session creation timestamp
updatedAt
timestamp
Last session update timestamp

3. Session Validation

On each authenticated request, the API validates the session:
// src/middleware/auth.ts
export const requireAuth = async (req, res, next) => {
  try {
    const session = await auth.api.getSession({
      headers: new Headers(req.headers)
    });

    if (!session) {
      return res.status(401).json({
        success: false,
        message: "Authentication required"
      });
    }

    req.user = session.user;
    req.session = session;
    next();
  } catch (error) {
    return res.status(401).json({
      success: false,
      message: "Invalid session"
    });
  }
};
The validation process:
  1. Extracts session token from cookie
  2. Queries the sessions table
  3. Verifies token signature and expiration
  4. Loads associated user data
  5. Attaches user to request object

Get Current Session

Retrieve the current user’s session information:
GET /api/session
curl -X GET https://api.yourdomain.com/api/session \
  -H "Content-Type: application/json" \
  -b cookies.txt
The -b cookies.txt flag includes the session cookie in the request.

Response

{
  "success": true,
  "data": {
    "user": {
      "id": "usr_1234567890",
      "email": "[email protected]",
      "name": "John Doe",
      "emailVerified": true,
      "image": "https://lh3.googleusercontent.com/...",
      "role": "user",
      "createdAt": "2024-03-15T10:30:00.000Z",
      "updatedAt": "2024-03-15T10:30:00.000Z"
    },
    "session": {
      "id": "ses_abcdefghijk",
      "expiresAt": "2024-04-14T10:30:00.000Z"
    }
  }
}

Implementation

The session endpoint is defined in src/index.ts:
app.get("/api/session", async (req, res) => {
  try {
    const session = await auth.api.getSession({
      headers: new Headers(req.headers as Record<string, string>),
    });

    if (!session) {
      return res.json({ success: true, data: null });
    }

    // Fetch complete user data including role from database
    const { db } = await import("./db");
    const { users } = await import("./db/schema");
    const { eq } = await import("drizzle-orm");

    const userWithRole = await db.query.users.findFirst({
      where: eq(users.id, session.user.id),
      columns: {
        id: true,
        name: true,
        email: true,
        emailVerified: true,
        image: true,
        createdAt: true,
        updatedAt: true,
        role: true,
      },
    });

    res.json({
      success: true,
      data: {
        user: userWithRole,
        session: {
          id: session.session.id,
          expiresAt: session.session.expiresAt,
        },
      },
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      message: "Failed to get session",
    });
  }
});

Session Lifecycle

Session Extension

Sessions are automatically extended if:
  1. The session age is older than updateAge (7 days)
  2. The session hasn’t expired yet
  3. The user makes an authenticated request
When extended:
  • expiresAt is updated to 30 days from now
  • updatedAt timestamp is refreshed
  • Session cookie Max-Age is reset

Session Expiration

Sessions expire after 30 days of inactivity. Expired sessions:
  • Are automatically cleaned up from the database
  • Return 401 Unauthorized on validation
  • Require the user to sign in again

Manual Session Termination

Users can manually end their session by signing out:
POST /api/auth/sign-out
curl -X POST https://api.yourdomain.com/api/auth/sign-out \
  -b cookies.txt
This:
  1. Deletes the session from the database
  2. Clears the session cookie
  3. Returns success response

Using Sessions in Requests

All authenticated endpoints require including the session cookie:
# Save cookies on sign-in
curl -X POST https://api.yourdomain.com/api/auth/sign-in/email \
  -c cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "password"}'

# Use cookies in subsequent requests
curl -X GET https://api.yourdomain.com/api/user/me \
  -b cookies.txt
Always include credentials: 'include' (fetch) or withCredentials: true (axios) to send cookies with cross-origin requests.

CORS Configuration

For cookies to work with cross-origin requests, CORS must be properly configured:
// src/index.ts
app.use(cors({
  origin: [process.env.FRONTEND_URL],
  credentials: true, // Allow cookies
}));

Trusted Origins

trustedOrigins: [
  process.env.FRONTEND_URL,     // e.g., https://yourdomain.com
  process.env.BETTER_AUTH_URL,  // e.g., https://api.yourdomain.com
]
Only requests from trusted origins can create and use sessions.

Client-Side Session Management

React Hook Example

import { useEffect, useState } from 'react';

interface User {
  id: string;
  email: string;
  name: string;
  role: 'user' | 'admin';
}

export function useSession() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    checkSession();
  }, []);

  const checkSession = async () => {
    try {
      const response = await fetch('https://api.yourdomain.com/api/session', {
        credentials: 'include'
      });
      const data = await response.json();
      
      if (data.success && data.data) {
        setUser(data.data.user);
      } else {
        setUser(null);
      }
    } catch (error) {
      console.error('Session check failed:', error);
      setUser(null);
    } finally {
      setLoading(false);
    }
  };

  const signOut = async () => {
    await fetch('https://api.yourdomain.com/api/auth/sign-out', {
      method: 'POST',
      credentials: 'include'
    });
    setUser(null);
  };

  return { user, loading, signOut, refreshSession: checkSession };
}

Usage

function Dashboard() {
  const { user, loading, signOut } = useSession();

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>Please sign in</div>;

  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <button onClick={signOut}>Sign Out</button>
    </div>
  );
}

Security Best Practices

Sessions use httpOnly cookies that cannot be accessed via JavaScript, protecting against XSS attacks:
defaultCookieAttributes: {
  httpOnly: true, // Prevents document.cookie access
}
In production, cookies require HTTPS:
useSecureCookies: process.env.NODE_ENV === "production",
defaultCookieAttributes: {
  secure: true, // HTTPS only
}
SameSite=None allows cross-origin requests but requires Secure=true:
defaultCookieAttributes: {
  sameSite: "none",  // Cross-origin allowed
  secure: true,       // Required with SameSite=None
}
Sessions are automatically refreshed every 7 days, limiting the window for session hijacking.
Sessions store IP address and user agent for anomaly detection:
session: {
  ipAddress: "192.168.1.1",
  userAgent: "Mozilla/5.0..."
}

Troubleshooting

Symptoms: User is logged out on page refreshCauses & Solutions:
  • Missing credentials: 'include' in fetch requests
  • CORS not configured with credentials: true
  • Frontend URL not in trustedOrigins
  • Browser blocking third-party cookies (use same domain or educate users)
  • SameSite=None requires Secure=true (use HTTPS)
Symptoms: Getting 401 on authenticated requestsCauses & Solutions:
  • Session expired (30 days) - user needs to sign in again
  • Session manually terminated - check sign-out calls
  • Cookie not being sent - verify credentials: 'include'
  • Invalid session token - clear cookies and sign in again
Symptoms: Browser blocks requests with CORS errorCauses & Solutions:
  • Frontend URL not in CORS origin array
  • Missing credentials: true in CORS config
  • Preflight request failing - ensure OPTIONS requests allowed
  • Mismatched protocols (http vs https)

Next Steps

Authentication Middleware

Learn how to protect routes with session validation

Email/Password Auth

Implement email and password authentication

OAuth (Google)

Add Google OAuth authentication

User Profile

Access user information from sessions

Build docs developers (and LLMs) love