Authentication
Secure hypequery endpoints with auth strategies, type-safe auth guards, and role-based access control
Authentication
hypequery provides a complete authentication and authorization system. You define auth strategies that extract user identity from requests, then use type-safe guards to control who can access each endpoint.
Core Concepts
- Auth strategies run first, extracting identity from requests (API keys, JWT tokens, cookies, etc.) and returning an auth context with
userId,roles, andscopes - Auth guards (
requireAuth(),requireRole(),requireScope()) control endpoint access after authentication succeeds - Type-safe auth with
createAuthSystemenables autocomplete and compile-time checking for roles and scopes - Auth context is injected into
ctx.authand available in queries, middleware, and tenant helpers - Failures return structured errors:
401 UNAUTHORIZEDfor missing auth,403 FORBIDDENfor wrong roles/scopes
Quick Example
Here's a complete example showing type-safe auth with role and scope-based access control:
import { initServe, createAuthSystem } from '@hypequery/serve';
import { z } from 'zod';
// ============================================================
// 1. Define your valid roles and scopes
// ============================================================
const { useAuth, TypedAuth } = createAuthSystem({
roles: ['admin', 'editor', 'viewer'] as const,
scopes: ['read:data', 'write:data', 'delete:data'] as const,
});
type AppAuth = typeof TypedAuth;
type AppContext = { db: any };
// ============================================================
// 2. Create an auth strategy
// ============================================================
const authStrategy = async ({ request }): Promise<AppAuth | null> => {
const token = request.headers?.['x-auth-token'];
if (!token) return null;
// Verify JWT token
const payload = await verifyJwt(token);
return {
userId: payload.sub,
userName: payload.name,
roles: payload.roles, // e.g., ['admin', 'viewer']
scopes: payload.scopes, // e.g., ['read:data', 'write:data']
};
};
// ============================================================
// 3. Define your API with auth guards
// ============================================================
const { define, query } = initServe<AppContext, AppAuth>({
context: () => ({ db: createDbConnection() }),
});
export const api = define({
auth: useAuth(authStrategy),
queries: {
// Public endpoint - anyone can access
public: query
.public()
.describe('Public health check')
.query(async () => ({
status: 'ok',
message: 'Anyone can access this',
})),
// Require authentication
profile: query
.requireAuth()
.describe('Get user profile')
.query(async ({ ctx }) => ({
userId: ctx.auth?.userId,
userName: ctx.auth?.userName,
roles: ctx.auth?.roles,
})),
// Require specific role (OR semantics - any listed role grants access)
adminOnly: query
.requireRole('admin', 'super-admin')
.describe('Admin-only endpoint')
.query(async ({ ctx }) => ({
message: 'Admin data',
data: await ctx.db.table('admin_data').select('*'),
})),
// Require specific scope (AND semantics - all listed scopes required)
writeData: query
.requireScope('write:data')
.describe('Write data endpoint')
.query(async ({ ctx, input }) => ({
success: true,
data: await ctx.db.table('data').insert(input),
})),
// Combine guards - needs both role AND scope
superAdmin: query
.requireRole('admin')
.requireScope('delete:data')
.describe('Super admin operation')
.query(async ({ ctx }) => ({
message: 'Super admin operation',
})),
},
});Auth Guards
Guards are declarative methods on the query builder that control authorization. They're checked after authentication succeeds.
.public()
Opt an endpoint out of authentication. The endpoint still attempts auth (so ctx.auth is populated if credentials are present) but never rejects missing credentials.
healthcheck: query
.public()
.query(async () => ({ status: 'ok' })),.requireAuth()
Require authentication. Returns 401 UNAUTHORIZED when no auth context is present.
profile: query
.requireAuth()
.query(async ({ ctx }) => ({
userId: ctx.auth?.userId,
})),.requireRole(...roles)
Require the user to have at least one of the listed roles (OR semantics). Returns 403 FORBIDDEN when no role matches.
adminDashboard: query
.requireRole('admin', 'super-admin')
.query(async ({ ctx }) => { /* ... */ }),.requireScope(...scopes)
Require the user to have all listed scopes (AND semantics). Returns 403 FORBIDDEN when any scope is missing.
sensitiveExport: query
.requireScope('read:data', 'export:data')
.query(async ({ ctx }) => { /* ... */ }),Combining Guards
Chain multiple guards to enforce complex requirements:
adminExport: query
.requireRole('admin') // Must be admin
.requireScope('export:data') // Must have export scope
.query(async ({ ctx }) => { /* ... */ }),Error Responses
Guards return structured errors that clients can branch on:
{
"error": {
"type": "FORBIDDEN",
"message": "Missing required role",
"details": {
"reason": "missing_role",
"required": ["admin"],
"actual": ["viewer"],
"endpoint": "/admin-dashboard"
}
}
}| Guard | HTTP Status | Error Type | Semantics |
|---|---|---|---|
.requireAuth() | 401 | UNAUTHORIZED | No credentials |
.requireRole() | 403 | FORBIDDEN | Wrong role (OR) |
.requireScope() | 403 | FORBIDDEN | Missing scope (AND) |
Type-Safe Auth with createAuthSystem
createAuthSystem enables TypeScript autocomplete and compile-time checking for role and scope values.
import { createAuthSystem } from '@hypequery/serve';
// Define your valid roles and scopes up front
const { useAuth, TypedAuth } = createAuthSystem({
roles: ['admin', 'editor', 'viewer'] as const,
scopes: ['read:data', 'write:data', 'delete:data'] as const,
});
// Extract the combined auth type
type AppAuth = typeof TypedAuth;Benefits
- Compile-time safety: Catch typos like
'admn'or'superadmin'at build time - IDE autocomplete: Valid roles and scopes are suggested as you type
- Refactoring: Rename roles and TypeScript shows all places that need updating
Using the Typed Auth Type
const jwtStrategy: AuthStrategy<AppAuth> = async ({ request }) => {
const token = request.headers.authorization?.slice(7);
const payload = await verifyJwt(token);
return {
userId: payload.sub,
roles: payload.roles, // ✅ Type-checked: must be from defined list
scopes: payload.scopes, // ✅ Type-checked: must be from defined list
};
};
const { define, query } = initServe<AppContext, AppAuth>({
auth: useAuth(jwtStrategy),
context: () => ({ db }),
});
export const api = define({
queries: {
// ✅ TypeScript autocomplete for 'admin'
// ❌ Compile error if you typo 'admn' or 'superadmin'
adminOnly: query
.requireRole('admin')
.query(async ({ ctx }) => ({ secret: true })),
},
});Auth Strategies
Strategies extract auth context from incoming requests. Return null for failed authentication.
API Key Strategy
const apiKeyStrategy = async ({ request }) => {
const key = request.headers['x-api-key'];
if (!key || key !== process.env.HQ_API_KEY) {
return null;
}
return {
userId: 'service:dashboard',
roles: ['internal'],
scopes: ['read:all'],
};
};JWT Bearer Token Strategy
const bearerStrategy = async ({ request }) => {
const header = request.headers.authorization;
if (!header?.startsWith('Bearer ')) return null;
const token = header.slice(7);
const payload = await verifyJwt(token);
return {
userId: payload.sub,
roles: payload.roles,
scopes: payload.scopes,
};
};Multiple Strategies
Provide an array to support multiple auth methods. Strategies run sequentially until one succeeds.
const { define } = initServe({
auth: [apiKeyStrategy, bearerStrategy],
context: () => ({ db }),
});Global Auth Configuration
Set auth globally in initServe or define:
const { define, query } = initServe<AppContext, AppAuth>({
auth: useAuth(authStrategy),
context: () => ({ db }),
});
export const api = define({
// All queries inherit auth strategy
queries: {
// This can use .public() to bypass global auth
healthcheck: query.public().query(async () => ({ ok: true })),
},
});Accessing Auth in Queries
Access auth context via ctx.auth:
profile: query
.requireAuth()
.query(async ({ ctx }) => ({
userId: ctx.auth?.userId,
roles: ctx.auth?.roles,
scopes: ctx.auth?.scopes,
})),Accessing Auth in Middleware
const logUserMiddleware = async (ctx, next) => {
console.log('User:', ctx.auth?.userId ?? 'anonymous');
console.log('Roles:', ctx.auth?.roles ?? []);
return next();
};
const { define } = initServe({
middlewares: [logUserMiddleware],
auth: authStrategy,
context: () => ({ db }),
});Embedded Execution
When calling api.run() directly (cron jobs, SSR), pass a synthetic request:
// With auth
const result = await api.run('profile', {
request: {
method: 'POST',
path: '/profile',
headers: { 'x-auth-token': 'admin-token' },
query: {},
},
});Bypassing auth in internal calls
For trusted internal calls, you can provide context directly to skip auth:
// Skip auth by providing context
const result = await api.run('adminOnly', {
context: {
db: yourDbConnection,
auth: { userId: 'system', roles: ['admin'] },
},
input: {},
});Framework Integration
Next.js App Router
// app/api/analytics/[...path]/route.ts
import { api } from '@/analytics/queries';
import { createFetchHandler } from '@hypequery/serve';
const handler = createFetchHandler(api.handler);
export const runtime = 'nodejs';
export const GET = handler;
export const POST = handler;Hono
// src/app.ts
import { api } from './analytics/queries';
import { createFetchHandler } from '@hypequery/serve';
import { Hono } from 'hono';
const app = new Hono();
const hypequery = createFetchHandler(api.handler);
app.all('/api/*', (c) => hypequery(c.req.raw));Security: Controlling Error Verbosity
By default, auth guards hide required and actual arrays to prevent information leakage. Enable verbose mode for development:
const { define } = initServe({
security: {
verboseAuthErrors: true, // Show missing roles/scopes (development only!)
},
auth: authStrategy,
context: () => ({ db }),
});When to use verbose mode:
- ✅ Development and debugging
- ✅ Internal/private APIs with trusted consumers
- ❌ Production APIs (prevents information leakage)
Hooks for Monitoring
Track authentication and authorization failures separately:
const { define } = initServe({
hooks: {
onAuthFailure: async ({ request, reason }) => {
// 401 - No credentials or invalid credentials
logger.warn('auth_failure', { reason, ip: request.headers['x-forwarded-for'] });
},
onAuthorizationFailure: async ({ auth, required, actual, reason }) => {
// 403 - Authenticated but wrong role/scope
logger.warn('authz_failure', {
userId: auth?.userId,
reason,
required,
actual,
});
},
},
auth: authStrategy,
context: () => ({ db }),
});Troubleshooting
Missing headers – Ensure your framework forwards headers to the hypequery handler. For Next.js or Vercel, copy Authorization/X-API-Key into the request.
Edge runtimes – Use createFetchHandler or createVercelEdgeHandler so headers/requests stay compatible with auth strategies.
Type errors – Use createAuthSystem to get autocomplete and catch typos at compile time.
With these patterns you can safely protect hypequery endpoints without tying yourself to a specific auth provider. Strategies are just functions—swap tokens, cookies, mTLS metadata, or anything your stack supports.