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)
endQué garantiza cada paso
- 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. - 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 siexpes pasado. Esto es la Hard Invariant #4. - Verificación de la clave padre. La
ss_search_*padre (o el padre dess_scoped_*) se hashea sha256 y se busca enSearchApiKey.hash. Las claves en texto plano nunca se almacenan — Hard Invariant #3. - Allow-list de origen. Cuando
allowedOriginsno está vacío, la cabeceraOriginse compara estrictamente. Los fallos devuelven 403 antes de tocar Typesense. - Rate limit. Ventana deslizante por clave; 429 al exceder.
- Combinación del filtro de tenant. El
filter_byde Typesense siempre combina por AND el filtro provisto por el cliente, el filtro del scoped-token (si lo hay) y la cláusulatenantId:<org>inyectada por el servidor. El filtro de tenant se añade en la capa SQL/TypesenseWHERE, nunca como cabecera — Hard Invariant #5. - Dispatch multi_search. Un único round trip a Typesense distribuye las entradas de búsqueda. Cada entrada DEBE llevar el mismo filtro de tenant.
- Saneamiento de la respuesta.
buildSearchResponseelimina campos internos, aplica las etiquetas de highlight configuradas y emite la forma pública de respuesta. - Analítica.
recordSearchUsageAsyncencola una filaSearchUsageEventde tiposearch_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_SECRETsolo en servidor), - una restricción
filterByque la API combinará por AND con cada petición (p. ej.user_id:42 && shop_id:abc), - un
expcorto (típicamente 15 minutos), para que un token filtrado tenga un radio de impacto pequeño.
Relacionado
- Tipos de claves y modelo de seguridad — comparación completa de las cuatro categorías de claves.
- Ruta de escritura — el flujo espejo para ingesta.
Ruta de escritura
Cómo una escritura de documento entra en AACsearch — desde POST /v1/indexes/.../documents a través de SearchIngestBuffer / SearchSyncOutbox y el worker, hasta el alias de Typesense.
Tipos de claves y modelo de seguridad
Las cuatro categorías de claves de AACsearch — search, connector, scoped y admin — sus prefijos ss_*, almacenamiento solo-hash, HMAC + TTL + filtro del scoped-token y cómo se verifica cada una en tiempo de petición.