AACsearch

Architecture

Monorepo layout, service boundaries, the layered backend principle, and the data flow for write and read paths.

AACsearch is built on a AACSearch platform using Turborepo with pnpm workspaces. The codebase is split into four apps and sixteen packages, all served through a single Hono HTTP process that mounts oRPC, public search endpoints, widget bundle, connectors, and auth.

Monorepo layout

apps/
  saas/           # Protected SaaS dashboard — indexes, keys, analytics, connectors, billing
  marketing/      # Public marketing site — landing, pricing, blog, changelog, legal
  docs/           # Public docs site (this site — v0.7)
  mail-preview/   # Email template preview only

packages/
  api/            # Hono + oRPC — 11 oRPC modules + public Hono routes
  auth/           # Better Auth — login, orgs, passkeys, 2FA, magic links, OAuth
  database/       # Prisma (active ORM) + Drizzle (legacy reference only) — 33 models
  search/         # AACSearch client, collections, buffer, reindex, keys, ingest
  search-client/  # Browser SDK — search-only, no admin keys
  widget/         # Hosted storefront widget — Vanilla JS, Shadow DOM, 14 KB IIFE+ESM
  i18n/           # 5 locales × 4 scopes (en/de/es/fr/ru × marketing/saas/mail/shared)
  payments/       # Stripe / LemonSqueezy / Polar / Creem / DodoPayments / Tochka
  billing-wallet/ # BigInt kopecks ledger, reserve→commit/release
  ai/             # Vercel AI SDK + provider configs
  ai-core/        # Lower-level AI orchestration primitives
  notifications/  # createNotification, list, mark-read, preferences, catalog
  mail/           # React Email templates + provider drivers
  storage/        # S3-compatible (MinIO for local dev)
  ui/             # 27  UI primitives
  logs/           # pino-based logger
  utils/          # Generic helpers

modules/          # NOT a Node workspace — PHP CMS modules
  prestashop/aacsearch/   # PrestaShop 8.x (skeleton, separate track)
  bitrix/aac.search/      # 1C-Bitrix self-hosted (skeleton, separate track)

HTTP server

All traffic enters through a single Hono app at packages/api/index.ts, which mounts routes in order:

MountPurpose
/apiPublic search handler (permissive CORS)
/apiWidget analytics events
/api/widget/widget.jsHosted widget JS bundle
/apiConnector API (CMS modules)
/apiAnalytics handler
/apiSCIM 2.0 (identity provisioning)
/api/v1REST API v1 (15 endpoints, OpenAPI 3.1)
/api/auth/**Better Auth handler
/api/webhooks/paymentsPayment provider webhooks
/api/rpc/**oRPC handler (11 modules)

oRPC modules

The oRPC router at packages/api/orpc/router.ts mounts 11 modules:

admin, organizations, users, payments, ai, notifications, search, knowledge, billingWallet, entitlements

The search module has 26 procedures covering indexes, API keys, scoped tokens, relevance, analytics, connectors, and import jobs.

The layered backend principle

External CMS modules and storefront browsers never contact AACSearch directly. The AACsearch API layer holds the only admin credentials.

PrestaShop / Bitrix module
    │  bearer(ss_connector_* token)

AACsearch Connector API  ←  validates, auth-checks, rate-limits


SearchIngestBuffer (Prisma)  ←  enqueueManySearchIngest()


Background worker  ←  drains buffer in batches


AACSearch  (server-side only, admin key never leaves the server)

Exposing a search admin key in a CMS module or <script> tag would allow arbitrary index manipulation across all tenants. This is Hard Invariant #2 in the codebase and is non-negotiable.

Write path (ingest)

sequenceDiagram
    autonumber
    participant Client
    participant API as public-handler.ts
    participant DB as SearchIngestBuffer (PG)
    participant W as Worker
    participant TS as Typesense
    Client->>API: POST /search/documents
    API->>API: auth + quota check
    API->>DB: enqueueManySearchIngest()
    API-->>Client: 202 Accepted
    loop tick
        W->>DB: SELECT unprocessed rows
        W->>TS: bulkUpsert(batch)
        alt success
            W->>DB: markIngestRowsSuccess
        else partial fail
            W->>DB: markIngestRowsFailure + exp. backoff
        end
    end

The DB-buffer approach provides durability (rows survive server restarts), partial-fail handling (only failed rows are retried), and batching (worker groups rows by collection before sending).

sequenceDiagram
    autonumber
    participant Client
    participant API as public-handler.ts
    participant PC as Policy cache
    participant TS as Typesense
    participant A as Analytics (SearchUsageEvent)
    Client->>API: POST /search
    API->>API: auth (Bearer token)
    API->>API: rate-limit (per-key, 1m sliding)
    API->>PC: resolve plan / scope (60s LRU)
    API->>TS: multi_search(scoped filter_by)
    TS-->>API: hits + facets
    API-->>Client: sanitized JSON
    API->>A: recordSearchUsage() (fire-and-forget)

Collection naming and versioning

Every search index uses versioned collection names to enable zero-downtime reindex:

Collection name:  {orgShortId}_{slug}_v{version}
Alias name:       {orgShortId}_{slug}

All search queries target the alias. A reindex creates a new version, verifies it, then atomically swaps the alias. The previous version stays alive until the next reindex confirms green.

flowchart LR
    Alias["alias: org_slug → v3"]
    V1["v1 (retired)"]
    V2["v2 (live, serves reads)"]
    V3["v3 (building)"]
    Build["reindex builds v3"] --> Verify["verify v3 docs"]
    Verify -->|green| Swap["atomic alias swap"]
    Swap --> AliasNew["alias → v3"]
    Alias -.->|before| V2
    AliasNew -.->|after| V3
    V2 -.->|kept until next reindex confirms green| V2

Technology choices

LayerTechnology
FrameworkNext.js 16 (App Router, RSC)
APIHono + oRPC 1.13
AuthBetter Auth 1.5
ORMPrisma 7 (active) / Drizzle (legacy reference)
Search engineAACSearch 3
ValidationZod 4
Client data fetchingTanStack Query 5
Formsreact-hook-form 7
UITailwind v4 + primitives
LintOxlint (not ESLint)
FormatOxfmt (not Prettier)
Package managerpnpm 10 with workspace catalog
Build orchestrationTurborepo 2

On this page