Authentication
Add authentication with API keys, typed auth guards, and shared auth context.
Authentication
Authentication in hypequery starts at the runtime layer. You attach auth strategies to initServe(...) or serve({ ... }), and the resolved auth object is made available as ctx.auth inside your query definitions.
Model
- define an auth strategy that reads request credentials
- return an auth object or
null - access the resolved auth object through
ctx.auth - optionally use
createAuthSystem(...)for typed roles and scopes - add per-query auth requirements directly in
query({ ... }) - reuse the same runtime for local execution, routes, docs, and framework handlers
API key example
import { createApiKeyStrategy, initServe } from '@hypequery/serve';
import { db } from './client';
const apiKeyAuth = createApiKeyStrategy({
header: 'x-api-key',
validate: async (key) => {
const account = await findApiKey(key);
if (!account) return null;
return {
userId: account.userId,
tenantId: account.tenantId,
role: account.role,
};
},
});
const { query, serve } = initServe({
context: () => ({ db }),
auth: apiKeyAuth,
basePath: '/api/analytics',
});
const tenantUsers = query({
requiresAuth: true,
query: ({ ctx }) =>
ctx.db
.table('users')
.select(['id', 'email', 'last_seen_at'])
.where('tenant_id', 'eq', ctx.auth!.tenantId)
.orderBy('last_seen_at', 'DESC')
.limit(50)
.execute(),
});
export const api = serve({
queries: { tenantUsers },
});Bearer token example
import { createBearerTokenStrategy, initServe } from '@hypequery/serve';
import { db } from './client';
const bearerAuth = createBearerTokenStrategy({
validate: async (token) => {
const payload = await verifyJwt(token);
return payload
? {
userId: payload.sub,
email: payload.email,
tenantId: payload.tenantId,
}
: null;
},
});
const { query, serve } = initServe({
context: () => ({ db }),
auth: bearerAuth,
});
const recentOrders = query({
requiresAuth: true,
query: ({ ctx }) =>
ctx.db
.table('orders')
.select(['id', 'status', 'total', 'created_at'])
.where('user_id', 'eq', ctx.auth!.userId)
.orderBy('created_at', 'DESC')
.limit(25)
.execute(),
});Where auth lives
- attach auth globally in
initServe(...)orserve({ ... }) - read the resolved auth object from
ctx.auth - combine auth with Multi-Tenancy when tenant identity comes from credentials
Per-query auth in query({ ... })
Use object-style auth fields when a reusable query definition should enforce access rules directly.
import { createAuthSystem, initServe } from '@hypequery/serve';
import { db } from './client';
const { useAuth, TypedAuth } = createAuthSystem({
roles: ['admin', 'editor'] as const,
scopes: ['read:data', 'write:data'] as const,
});
type AppAuth = typeof TypedAuth;
const authStrategy = async ({ request }): Promise<AppAuth | null> => {
const token = request.headers['x-auth-token'];
if (!token) return null;
const payload = await verifyJwt(token);
return {
userId: payload.sub,
roles: payload.roles,
scopes: payload.scopes,
};
};
const { query, serve } = initServe({
context: () => ({ db }),
auth: useAuth(authStrategy),
});
const adminMetrics = query({
description: 'Admin-only revenue metrics',
requiredRoles: ['admin'],
requiredScopes: ['read:data'],
query: async ({ ctx }) =>
ctx.db
.table('metrics')
.select(['name', 'value', 'updated_at'])
.orderBy('updated_at', 'DESC')
.limit(20)
.execute(),
});
const health = query({
requiresAuth: false,
query: async () => ({ ok: true }),
});
export const api = serve({
queries: { adminMetrics, health },
});Semantics:
requiresAuth: falsemakes a query publicrequiresAuth: truerequires an authenticated userrequiredRoles: ['admin', 'editor']uses OR semanticsrequiredScopes: ['read:data', 'write:data']uses AND semanticsrequiredRolesorrequiredScopesimply auth automatically
Typed authorization with createAuthSystem
Use createAuthSystem(...) when you want compile-time safety for roles and scopes.
import { createAuthSystem, initServe } from '@hypequery/serve';
const { useAuth, TypedAuth } = createAuthSystem({
roles: ['admin', 'editor'] as const,
scopes: ['read:data', 'write:data'] as const,
});
type AppAuth = typeof TypedAuth;
const authStrategy = async ({ request }): Promise<AppAuth | null> => {
const token = request.headers['x-auth-token'];
if (!token) return null;
const payload = await verifyJwt(token);
return {
userId: payload.sub,
roles: payload.roles,
scopes: payload.scopes,
};
};
const { query, serve } = initServe({
context: () => ({ db }),
auth: useAuth(authStrategy),
});
const adminMetrics = query({
requiredRoles: ['admin'],
query: async ({ ctx }) =>
ctx.db
.table('metrics')
.select(['name', 'value'])
.orderBy('value', 'DESC')
.limit(10)
.execute(),
});This gives you:
- autocomplete for valid roles and scopes
- compile-time checking for
requiredRolesandrequiredScopes - a typed
ctx.authshape across auth strategies, queries, and middleware
Notes
Headers are plain objects
Auth strategies receive a ServeRequest whose headers are plain objects, not Fetch Headers. Use request.headers.authorization or request.headers['x-api-key'].
Guard methods
The query builder-compatible auth guards are still current and supported:
.requireAuth().requireRole(...).requireScope(...).public()
Use object-style auth fields by default on query({ ... }). Use the chainable guard methods when you prefer the builder-style query surface or need backwards compatibility with existing guard-heavy definitions.