Skip to content

Hasura Integration

The auth service integrates with Hasura via a proxy pattern rather than Hasura’s built-in JWT mode. Your app verifies the JWT locally, then forwards GraphQL requests to Hasura with admin-secret headers that include the user’s identity.

Browser → App's /api/graphql → JWT verify → Hasura (with admin headers)

This approach gives you:

  • Full control over JWT verification logic
  • No JWT configuration needed in Hasura
  • Ability to refresh tokens transparently
  • CSRF protection via Origin header validation
app/api/graphql/route.ts
const secret = new TextEncoder().encode(process.env.JWT_SECRET!);
async function getAuthUser() {
const jar = await cookies();
const token = jar.get('yourapp_access_token')?.value;
if (!token) return null;
try {
const { payload } = await jwtVerify(token, secret);
if (typeof payload.sub !== 'string') return null;
return {
userId: payload.sub,
role: (payload.role as string) || 'user',
};
} catch {
return null;
}
}
export async function POST(request: Request) {
// CSRF check
const origin = request.headers.get('origin');
if (!origin || !ALLOWED_ORIGINS.includes(origin)) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Auth check
const auth = await getAuthUser();
if (!auth) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
// Forward to Hasura with admin secret + user context
const hasuraRes = await fetch(
`${process.env.HASURA_GRAPHQL_ENDPOINT}/v1/graphql`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET!,
'x-hasura-role': 'user',
'x-hasura-user-id': auth.userId,
},
body: JSON.stringify({
query: body.query,
variables: body.variables,
}),
},
);
const data = await hasuraRes.json();
return NextResponse.json(data);
}

Since the proxy sends x-hasura-role: user and x-hasura-user-id, configure Hasura permissions to use session variables:

hasura/metadata/databases/neon/tables/public_workouts.yaml
select_permissions:
- role: user
permission:
filter:
user_id:
_eq: X-Hasura-User-Id
columns: [id, name, finished_at, created_at]

This ensures users can only access their own data.

Always validate the Origin header before processing GraphQL requests:

const ALLOWED_ORIGINS = [
'https://yourapp.beshoy.ai',
...(process.env.NODE_ENV !== 'production'
? ['http://localhost:3006']
: []),
];
function validateOrigin(request: Request): boolean {
const origin = request.headers.get('origin');
if (!origin) return false;
return ALLOWED_ORIGINS.includes(origin);
}

For server-side operations (seeding, background jobs), bypass the proxy and call Hasura directly with the admin secret:

// lib/graphql/client.ts (server-only)
export async function graphql<T>(
query: string,
variables?: Record<string, unknown>,
) {
const res = await fetch(
`${process.env.HASURA_GRAPHQL_ENDPOINT}/v1/graphql`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_GRAPHQL_ADMIN_SECRET!,
},
body: JSON.stringify({ query, variables }),
},
);
return res.json() as Promise<{ data: T; errors?: unknown[] }>;
}