> hypequery

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(...) or serve({ ... })
  • 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: false makes a query public
  • requiresAuth: true requires an authenticated user
  • requiredRoles: ['admin', 'editor'] uses OR semantics
  • requiredScopes: ['read:data', 'write:data'] uses AND semantics
  • requiredRoles or requiredScopes imply 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 requiredRoles and requiredScopes
  • a typed ctx.auth shape 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.

See Also

On this page