AACsearch
Arquitectura

Ruta de lectura

Cómo una consulta de búsqueda fluye por AACsearch — desde el SDK del cliente hasta /search/public/multi, pasando por public-auth y la combinación de filtro de tenant, hacia el multi_search de Typesense y de vuelta como respuesta saneada.

La ruta de lectura es sin estado y caliente: no se escribe ninguna fila en PostgreSQL en la petición síncrona de búsqueda (los eventos de uso se registran fire-and-forget). Toda la cadena — auth, combinación del filtro scoped, combinación del filtro de tenant, dispatch multi_search — está diseñada para añadir el menor overhead posible sobre la llamada subyacente a Typesense.

Flujo

Descripción

El diagrama traza una petición pública de búsqueda desde el SDK del cliente a través de la verificación del token (scoped-token comprobado por HMAC o clave padre buscada por hash), las gates de origen y rate-limit, y luego por combineTenantFilter, que combina con AND el filtro del usuario, el filtro del scoped-token y la cláusula tenantId inyectada por el servidor antes de despachar multi_search a Typesense. La respuesta saneada se devuelve de forma síncrona mientras una fila SearchUsageEvent se encola de forma asíncrona para analítica.

sequenceDiagram
    autonumber
    participant SDK as Customer SDK / Widget
    participant API as packages/api/modules/search/public-handler.ts
    participant Gate as gatePublicSearchRequest
    participant ST as verifyScopedSearchToken (HMAC + exp)
    participant Key as verifySearchApiKey (hash-only)
    participant RL as SearchRateLimitBucket
    participant Filt as combineTenantFilter
    participant TS as Typesense multi_search
    participant Ev as SearchUsageEvent (async)

    SDK->>API: POST /api/search/public/multi
    API->>Gate: Bearer header (ss_search_* OR ss_scoped_*)
    alt ss_scoped_* prefix
        Gate->>ST: decode + HMAC verify + exp check
        ST-->>Gate: { parentRawKey, filterBy (scoped), keyId }
    end
    Gate->>Key: verifySearchApiKey(parentRawKey, scope=search)
    Key-->>Gate: VerifiedSearchKey { organizationId, indexSlug }
    Gate->>Gate: origin allow-list check
    Gate->>RL: increment + compare rateLimitPerMinute
    Gate-->>API: { verified, scopedFilter }
    API->>Filt: combine(userFilter && tenantFilter && scopedFilter)
    Filt-->>API: final filter_by expression
    API->>TS: multi_search(searches[], common_params)
    TS-->>API: hits + facets per search entry
    API->>API: buildSearchResponse (PII strip, highlight tags)
    API-->>SDK: 200 sanitized JSON
    par fire-and-forget
        API->>Ev: recordSearchUsageAsync(search_query)
    end

Qué garantiza cada paso

  1. Forma del token. Solo se aceptan tres prefijos: ss_search_*, ss_scoped_* y (para escrituras de conector) ss_connector_*. La verificación de forma ocurre antes de cualquier lectura en BD.
  2. Verificación del scoped-token. Los tokens ss_scoped_* llevan una firma HMAC-SHA256 sobre una carga útil codificada en base64url { keyId, parentRawKey, filterBy, exp }. El servidor compara la firma en tiempo constante, decodifica la clave padre desde dentro y rechaza si exp es pasado. Esto es la Hard Invariant #4.
  3. Verificación de la clave padre. La ss_search_* padre (o el padre de ss_scoped_*) se hashea sha256 y se busca en SearchApiKey.hash. Las claves en texto plano nunca se almacenan — Hard Invariant #3.
  4. Allow-list de origen. Cuando allowedOrigins no está vacío, la cabecera Origin se compara estrictamente. Los fallos devuelven 403 antes de tocar Typesense.
  5. Rate limit. Ventana deslizante por clave; 429 al exceder.
  6. Combinación del filtro de tenant. El filter_by de Typesense siempre combina por AND el filtro provisto por el cliente, el filtro del scoped-token (si lo hay) y la cláusula tenantId:<org> inyectada por el servidor. El filtro de tenant se añade en la capa SQL/Typesense WHERE, nunca como cabecera — Hard Invariant #5.
  7. Dispatch multi_search. Un único round trip a Typesense distribuye las entradas de búsqueda. Cada entrada DEBE llevar el mismo filtro de tenant.
  8. Saneamiento de la respuesta. buildSearchResponse elimina campos internos, aplica las etiquetas de highlight configuradas y emite la forma pública de respuesta.
  9. Analítica. recordSearchUsageAsync encola una fila SearchUsageEvent de tipo search_query. La promesa intencionalmente no se aguarda.

Por qué scoped-tokens

Un scoped-token es la única forma segura de embeber una caja de búsqueda gestionada por AACsearch en una página orientada al cliente. Lleva:

  • un HMAC estable que el cliente no puede falsificar (BETTER_AUTH_SECRET solo en servidor),
  • una restricción filterBy que la API combinará por AND con cada petición (p. ej. user_id:42 && shop_id:abc),
  • un exp corto (típicamente 15 minutos), para que un token filtrado tenga un radio de impacto pequeño.

Relacionado

On this page