Skip to content

Better Auth Integration

Better Auth is a framework-agnostic authentication library for TypeScript. This guide shows how to protect your Tabula Lens endpoint with Better Auth and pass session credentials from the frontend.

Install the required packages.

Backend:

Terminal window
npm i better-auth

Frontend:

Terminal window
npm i better-auth

Set the required environment variables:

Terminal window
# Generate with: openssl rand -base64 32
BETTER_AUTH_SECRET=your-secret-key-minimum-32-characters
BETTER_AUTH_URL=http://localhost:3000

Create a shared auth instance. Better Auth supports multiple database adapters — the example below uses a generic placeholder; refer to the Better Auth docs for your specific adapter.

lib/auth.ts
import { betterAuth } from 'better-auth';
export const auth = betterAuth({
database: { /* your DB adapter */ },
emailAndPassword: { enabled: true },
// Optional: social providers
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
});

The integration pattern is the same for all frameworks: validate the session before passing the request to the Tabula Lens handler.

Mount the Better Auth handler and add session validation to the Tabula Lens route:

app/api/auth/[...all]/route.ts
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';
export const { GET, POST } = toNextJsHandler(auth);
app/api/tabula-lens/route.ts
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
import { TabulaLens, createNextRouteHandler } from '@tabula-lens/node';
const tabulaLens = new TabulaLens(process.env.DATABASE_URL!);
const handler = createNextRouteHandler(tabulaLens);
export async function GET(request: Request) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return new Response('Unauthorized', { status: 401 });
}
return handler(request);
}

Mount the Better Auth handler and add an auth middleware before the Tabula Lens adapter:

import express from 'express';
import { fromNodeHeaders, toNodeHandler } from 'better-auth/node';
import { auth } from './lib/auth';
import { TabulaLens, expressAdapter } from '@tabula-lens/node';
const app = express();
const tabulaLens = new TabulaLens(process.env.DATABASE_URL!);
// Mount the Better Auth handler (Express v5 syntax)
// For Express v4, use '/api/auth/*' instead
app.all('/api/auth/{*any}', toNodeHandler(auth));
// Auth middleware
async function requireAuth(req, res, next) {
const session = await auth.api.getSession({
headers: fromNodeHeaders(req.headers),
});
if (!session?.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
(req as any).user = session.user;
next();
}
// Protect the Tabula Lens endpoint
app.use('/api/tabula-lens', requireAuth, expressAdapter(tabulaLens));

Create an auth client and use the session state to gate the viewer. Better Auth uses session cookies by default, so no token needs to be forwarded for same-origin requests — the browser sends the cookie automatically.

lib/auth-client.ts
import { createAuthClient } from 'better-auth/react';
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL!,
});
components/TabulaLensViewer.tsx
'use client';
import { authClient } from '@/lib/auth-client';
import { DatabaseViewer } from '@tabula-lens/react';
export function TabulaLensViewer() {
const { data: session, isPending } = authClient.useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Sign in to view data</div>;
return (
<DatabaseViewer
path="/api/tabula-lens"
getAuthHeaders={async () => ({})}
// Session cookie is sent automatically for same-origin requests
/>
);
}
  • Cookie-based sessions — Better Auth sends session data via HttpOnly cookies. Same-origin API routes receive the cookie automatically; getAuthHeaders can return an empty object in that case.
  • Cross-origin or mobile clients — Enable the bearer plugin on the server and use bearerClient on the client to exchange a session token via the Authorization header instead.
  • Database queries per request — Each auth.api.getSession() call hits the database. Enable cookie caching to reduce the load:
    lib/auth.ts
    export const auth = betterAuth({
    // ...
    session: {
    cookieCache: {
    enabled: true,
    maxAge: 60 * 60, // 1 hour
    },
    },
    });
  • Schema migrations — Re-run npx @better-auth/cli@latest migrate whenever you add or remove plugins, as some plugins require additional database tables.